No response compression for API JSON #92
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: LOW
The problem
No response compression.
backend/app/main.py:16-31adds noGZipMiddleware, and no reverseproxy exists yet (#50 is open).
A 100-row photo list is roughly 60-80 KB of JSON, and includes every field —
sha256,notes_current, and storage-derived fields the grid never displays — against the five fieldsPhotoTileactually uses.Fix
app.add_middleware(GZipMiddleware, minimum_size=1024)now. Roughly 8-10x reduction on JSON.while defining response models.
Done when
References
backend/app/main.py:16-31backend/app/api/routes/photos.py(_photo_to_dict)Done in
ae19464.backend/tests/test_compression.py, 11 tests.GZipMiddlewarewithminimum_size=1024— a higher floor than the default 500, because below a kilobyte the gzip header and the CPU outweigh the saving and a compressed 200-byte error envelope is slower in every dimension than the original.The exclusion turned out to be the half worth care
Starlette excludes only
text/event-streamby default, so without an opt-out the originals would be compressed too. Two separate problems:206 Partial Contentcarries aContent-Rangedescribing bytes the client is not receiving. A client assembling a file from ranges would splice compressed bytes into the middle of an irreplaceable original, and nothing would report a problem.Media responses now declare
Content-Encoding: identity— the standard way to say "do not encode this", and what the middleware actually checks. Both cases are tested, including the 206.Middleware ordering, which I got wrong first
RequestLogMiddleware(#89) is aBaseHTTPMiddleware, and those convert every response into a streaming one. Starlette honoursminimum_sizeonly for a body that arrives in a single piece — so with the compressor sitting outside the logger, a 28-byte 404 was being gzipped. The compressor is now inside it. Worth recording because it is invisible from the configuration:minimum_size=1024was set correctly and simply did nothing.The second condition — the documented reason
The issue offers a slim list serialization or a documented reason. This is the reason.
PhotoOutis one typed shape per resource, which is the whole of #76: the frontend's types are generated from it. A second shape would mean two contracts to keep in step, two sets of generated types, and a client that has to know which endpoint handed it which object — and the photo browser and the review workspace are looking at the same photograph.What made the original argument compelling — 60–80 KB dominated by repeated keys, repeated status strings, and 64-character hex digests the grid never renders — is exactly what gzip removes.
sha256alone is 3.8 KB across sixty rows and is the single most compressible thing in the payload.TestTheListShapeIsDeliberateholds that reasoning to account: it asserts the list and detail shapes stay identical, and measures the ratio. If compression ever stops being enough, those numbers are what will say so rather than someone re-litigating it from memory.Done when
Also asserted: a client that does not ask gets plain JSON, the body survives the round trip byte-identically, and #85's caching headers still work alongside it (compression is exactly the kind of change that breaks a validator).
Worth revisiting when #50 lands, as the issue says — a production proxy may want to own this, at which point the middleware becomes redundant rather than wrong.
1109 passed, 8 skipped; 7 e2e; ruff clean.