Use `zlib.compressobj` instead of `GzipFile` in `GZipMiddleware`
Summary
Replace the gzip.GzipFile + io.BytesIO machinery in GZipResponder with zlib.compressobj using wbits=16 + zlib.MAX_WBITS, which produces the same gzip wire format (header, CRC32 and size trailer).
Rationale
The GzipFile approach dates back to the original middleware commit (#111, 2018) and requires supporting machinery that compressobj makes unnecessary:
- the
io.BytesIObuffer and per-chunkgetvalue()/seek(0)/truncate()dance - the
ExitStacklifecycle and__call__override added in #3407 (acompressobjholds no resources that need closing) GzipFile's internalBufferedWriter, which hides when compression work actually happens
Unlike GzipFile.write(), compressobj.compress() consumes and deflates its entire input during the call, making the compression cost explicit and self-contained. This also simplifies any future work offloading large-chunk compression to a worker thread (#3410), which currently needs a Z_NO_FLUSH workaround to drain GzipFile's buffer inside the worker. This is the same primitive aiohttp uses in its ZLibCompressor.
Net: 17 insertions, 34 deletions.
Behavior
- Standard responses: byte-identical semantics; output is valid gzip with correct
Content-Encoding/Content-Length. - Streaming responses: each chunk is flushed with
Z_SYNC_FLUSH, matching the previousGzipFile.flush()behavior, so every streamed chunk remains immediately decodable by clients. - The gzip header no longer embeds a timestamp (
mtime=0equivalent), making compressed output deterministic for identical input.
Performance
No measurable change: 10 MiB level-9 response ~153 ms before and after; 160Ć64 KiB streamed chunks at level 6: 19.7 ms vs 19.1 ms (noise).
Validation
tests/middleware/test_gzip.py: 16 passedscripts/check: mypy and ruff clean