Summary
Hosted sites were the one HTTP surface with no compression. Server.Handler
dispatches a site host to server/pages before Web.Middleware runs, so a site
— a static file server, the best case gzip has — was sending raw HTML, CSS and
JavaScript, while the forge UI next door was compressed.
What changed
server/web/compress.gomoved tointernal/compressso both surfaces share one implementation of the negotiation and header rules instead of a copy. Two entry points, because the callers differ:compress.New(rw, r)for the forge, which needs the writer inside its middleware: the status recorder under it feeds the page-view metric and the panic path, so the compressor has to sit above it rather than wrap the whole handler;compress.Handler(next)for the site dispatcher, which is a plain wrap-serve-close.
server/server.go: the pages dispatcher is wrapped once, at the point both surfaces pass through.docs/operations.md: the compression baseline now says hosted sites are covered too.
Nothing about the rules changed: gzip only when asked with q > 0, text-like types
only, no Range, no already-encoded body, no tiny body, no 1xx/204/206/304/301,
and a handler can still opt out with Cache-Control: no-transform.
Why this is safe for a site
- A site’s
ETag(the blob sha) andLast-Modifiedsurvive, so revalidation is still a 304 off the blob sha;Vary: Accept-Encodingnames the representation. - A 304 and a directory’s 301 redirect carry no body to compress.
- A client that does not ask for gzip gets the exact bytes and the exact
Content-Length— the new test asserts the length equals the body for the identity case. - When a compressed body is small enough that net/http buffers it, the
Content-Lengthit adds is the gzipped length, never the file’s. The test says so explicitly, because that distinction is exactly what a reader would get wrong.
Measurements
A hosted index.html of ~1.5 KB of repeated markup: 83 bytes gzipped, over
the site host, with the ETag unchanged.
Verification
gofmt,go vet ./...,go test ./...— clean../vole workflow run verify—ok: 1 job(s).- New:
TestSiteResponsesAreCompressed(compressed page, identity client with an exactContent-Length, 304 with no encoding), plus the unit suite that moved with the package (22 negotiation cases, flush, unwrap). - Existing site tests are untouched and still pass; the
pagespackage unit test that assertsContent-Lengthruns below the dispatcher, so it still checks the file server rather than the compressor.
Base
This sits on contrib/keenan/prism-and-compression (#60), which is where the
compressor exists at all. Retarget when the lower requests land:
./vole change update <id> --base main
Unlike #60, the head branch is a plain name (site-compression): contrib/* is
the contributor namespace, and this account is a repo admin, so it only put the
tip behind the contributor CI gate for nothing. That is also why this request’s
branches are named the way they are.
Not in this change
- Brotli. Measured at +3.1 MB of binary for the Go encoder (the static dictionary alone is ~250 KB), to save ~40 KB per reader against gzip. The only version worth having is precompressing the static assets at vendor time, which is a separate decision.