Files
static/compress_test.go
zeekayandHanzo Dev 0ed50e4201 serve: 1.67 MB of text was going out uncompressed
The server had no content-encoding path at all. cloud.hanzo.ai answered a
198 KB document, 180 KB of CSS and 1.24 MB of JS byte-identical whether the
client asked for gzip or not — every surface on this image paid full freight
for text that is ~10% entropy.

Measured on that site's own export, 27 files, one cold load:
1,586,676 -> 440,328 bytes, 72% off. Document 198,191 -> 20,304 (89%),
Tailwind sheet 146,300 -> 20,701 (85%), largest JS chunk 223,314 -> 61,954
(72%). The two woff2 and one 525-byte chunk are correctly left alone.

Compress sits outside the file server, not inside it: the decision is about
the response, so the directory listing and the 404 page get it too. It
decides at WriteHeader, the first moment Content-Type is known.

Four requests are handed the raw writer. No gzip in Accept-Encoding (and
`gzip;q=0` is a refusal, which a substring test reads backwards). A Range
request, because offsets into the identity body do not address a compressed
stream. A HEAD, because it has no body — wrapping it made net/http count the
gzip envelope and announce Content-Length: 23 for a 146 KB stylesheet. And
anything already entropy-coded, by Content-Type allowlist rather than
denylist, since re-gzipping a png spends CPU to grow it.

Vary: Accept-Encoding goes on every compressible response including the ones
left identity, or a shared cache hands the gzip copy to a client that cannot
read it. Level 6, not 9: on this payload 9 buys under 2% for triple the CPU,
and nothing is cached server-side.

Still zero dependencies.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 15:13:08 -07:00

255 lines
8.4 KiB
Go

package static
import (
"bytes"
"compress/gzip"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// serveDir stands up the real file handler over a temp root, wrapped exactly as
// cmd/static wraps it. Every assertion below therefore describes the shipped
// composition, not a mock of it.
func serveDir(t *testing.T, files map[string]string) http.Handler {
t.Helper()
dir := t.TempDir()
for name, body := range files {
p := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
h, err := New(context.Background(), http.NotFoundHandler(), &Config{
Root: dir,
IndexFiles: []string{"index.html"},
}, "test")
if err != nil {
t.Fatal(err)
}
return Compress(h)
}
func get(h http.Handler, path string, hdr map[string]string) *httptest.ResponseRecorder {
r := httptest.NewRequest(http.MethodGet, path, nil)
for k, v := range hdr {
r.Header.Set(k, v)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w
}
func TestGzipRoundTrip(t *testing.T) {
body := strings.Repeat("<p>the same paragraph, many times over</p>\n", 400) // 16.8 KB
h := serveDir(t, map[string]string{"index.html": body})
w := get(h, "/index.html", map[string]string{"Accept-Encoding": "gzip, deflate, br"})
if got := w.Header().Get("Content-Encoding"); got != "gzip" {
t.Fatalf("Content-Encoding = %q, want gzip", got)
}
if got := w.Header().Get("Vary"); !strings.Contains(got, "Accept-Encoding") {
t.Errorf("Vary = %q, want it to name Accept-Encoding", got)
}
if got := w.Header().Get("Content-Length"); got != "" {
t.Errorf("Content-Length = %q — the identity length must not survive", got)
}
if got := w.Header().Get("Accept-Ranges"); got != "" {
t.Errorf("Accept-Ranges = %q — identity offsets do not address a gzip stream", got)
}
// Snapshot the wire bytes BEFORE decompressing: gzip.NewReader(w.Body)
// drains the buffer, and a length read afterwards is 0 — which passes a
// "smaller than the original" assertion for the wrong reason.
wire := append([]byte(nil), w.Body.Bytes()...)
zr, err := gzip.NewReader(bytes.NewReader(wire))
if err != nil {
t.Fatalf("body is not gzip: %v", err)
}
got, err := io.ReadAll(zr)
if err != nil {
t.Fatal(err)
}
if string(got) != body {
t.Fatal("decompressed body differs from the file on disk")
}
if len(wire) == 0 || len(wire) >= len(body) {
t.Fatalf("compressed %d bytes is not smaller than %d", len(wire), len(body))
}
t.Logf("html %d -> %d bytes (%.0f%% off)", len(body), len(wire),
100*(1-float64(len(wire))/float64(len(body))))
}
func TestIdentityWhenNotAccepted(t *testing.T) {
body := strings.Repeat("x", 40000)
h := serveDir(t, map[string]string{"a.css": body})
w := get(h, "/a.css", nil)
if got := w.Header().Get("Content-Encoding"); got != "" {
t.Errorf("Content-Encoding = %q with no Accept-Encoding", got)
}
if w.Body.String() != body {
t.Error("body was altered for a client that asked for nothing")
}
}
// `gzip;q=0` is the one spelling of "gzip" that means no, and a substring test
// gets it backwards.
func TestGzipQZeroIsRefusal(t *testing.T) {
h := serveDir(t, map[string]string{"a.js": strings.Repeat("x", 40000)})
w := get(h, "/a.js", map[string]string{"Accept-Encoding": "gzip;q=0, identity"})
if got := w.Header().Get("Content-Encoding"); got != "" {
t.Errorf("Content-Encoding = %q for gzip;q=0", got)
}
}
// A range of the compressed stream is not the range that was asked for.
func TestRangeStaysIdentity(t *testing.T) {
body := strings.Repeat("abcdefgh", 5000)
h := serveDir(t, map[string]string{"a.txt": body})
w := get(h, "/a.txt", map[string]string{"Accept-Encoding": "gzip", "Range": "bytes=0-7"})
if got := w.Header().Get("Content-Encoding"); got != "" {
t.Errorf("Content-Encoding = %q on a Range request", got)
}
if w.Code != http.StatusPartialContent {
t.Errorf("status = %d, want 206", w.Code)
}
if w.Body.String() != "abcdefgh" {
t.Errorf("body = %q, want the first 8 bytes", w.Body.String())
}
}
// Re-compressing a PNG spends CPU to produce more bytes than it started with.
func TestAlreadyCompressedTypesUntouched(t *testing.T) {
png := "\x89PNG\r\n\x1a\n" + strings.Repeat("\x00\x01\x02\x03", 8000)
h := serveDir(t, map[string]string{"a.png": png})
w := get(h, "/a.png", map[string]string{"Accept-Encoding": "gzip"})
if got := w.Header().Get("Content-Encoding"); got != "" {
t.Errorf("Content-Encoding = %q for image/png", got)
}
if got := w.Header().Get("Vary"); strings.Contains(got, "Accept-Encoding") {
t.Errorf("Vary = %q — a type that never varies must not fragment caches", got)
}
}
// Under one MTU the frame count is the same either way, and gzip's 18-byte
// envelope can make the answer bigger.
func TestTinyBodiesUntouched(t *testing.T) {
h := serveDir(t, map[string]string{"a.html": "<p>hi</p>"})
w := get(h, "/a.html", map[string]string{"Accept-Encoding": "gzip"})
if got := w.Header().Get("Content-Encoding"); got != "" {
t.Errorf("Content-Encoding = %q for a 9-byte body", got)
}
if w.Body.String() != "<p>hi</p>" {
t.Errorf("body = %q", w.Body.String())
}
}
// A 304 carries no body. Writing a gzip header and an empty member onto one
// produces a response no client can parse.
func TestNotModifiedCarriesNoEncoding(t *testing.T) {
h := serveDir(t, map[string]string{"a.html": strings.Repeat("<p>x</p>", 4000)})
first := get(h, "/a.html", map[string]string{"Accept-Encoding": "gzip"})
lm := first.Header().Get("Last-Modified")
if lm == "" {
t.Fatal("no Last-Modified to revalidate against")
}
w := get(h, "/a.html", map[string]string{"Accept-Encoding": "gzip", "If-Modified-Since": lm})
if w.Code != http.StatusNotModified {
t.Fatalf("status = %d, want 304", w.Code)
}
if got := w.Header().Get("Content-Encoding"); got != "" {
t.Errorf("Content-Encoding = %q on a 304", got)
}
if w.Body.Len() != 0 {
t.Errorf("304 wrote %d body bytes", w.Body.Len())
}
}
// The 404 page is a handler that writes its own status and streams its body —
// a different path through the wrapper than ServeContent takes.
func TestErrorPageCompresses(t *testing.T) {
dir := t.TempDir()
body := strings.Repeat("<p>not found</p>\n", 500)
if err := os.WriteFile(filepath.Join(dir, "404.html"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
h, err := New(context.Background(), http.NotFoundHandler(), &Config{
Root: dir, IndexFiles: []string{"index.html"}, ErrorPage404: "404.html",
}, "test")
if err != nil {
t.Fatal(err)
}
w := get(Compress(h), "/nope", map[string]string{"Accept-Encoding": "gzip"})
if w.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404", w.Code)
}
if got := w.Header().Get("Content-Encoding"); got != "gzip" {
t.Fatalf("Content-Encoding = %q, want gzip", got)
}
zr, err := gzip.NewReader(bytes.NewReader(w.Body.Bytes()))
if err != nil {
t.Fatalf("404 body is not gzip: %v", err)
}
got, _ := io.ReadAll(zr)
if string(got) != body {
t.Error("404 body did not survive the round trip")
}
}
// A HEAD carries no body, so its Content-Length must describe the resource. The
// first cut of this wrapper let gzip close over the empty stream and net/http
// counted the 20-byte envelope: a 146 KB stylesheet advertised 23 bytes.
func TestHeadAdvertisesTheResourceLength(t *testing.T) {
body := strings.Repeat("<p>x</p>\n", 5000) // 45000 bytes
h := serveDir(t, map[string]string{"a.html": body})
r := httptest.NewRequest(http.MethodHead, "/a.html", nil)
r.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if got := w.Header().Get("Content-Encoding"); got != "" {
t.Errorf("Content-Encoding = %q on a HEAD", got)
}
if got := w.Header().Get("Content-Length"); got != "45000" {
t.Errorf("Content-Length = %q, want 45000 (the resource, not a gzip envelope)", got)
}
}
func TestCompressibleClassification(t *testing.T) {
yes := []string{
"text/html; charset=utf-8", "text/css", "application/javascript",
"text/javascript; charset=utf-8", "application/json", "image/svg+xml",
"application/manifest+json", "application/wasm", "TEXT/PLAIN",
}
no := []string{
"image/png", "image/jpeg", "font/woff2", "video/mp4",
"application/gzip", "application/octet-stream", "",
}
for _, ct := range yes {
if !compressible(ct) {
t.Errorf("compressible(%q) = false", ct)
}
}
for _, ct := range no {
if compressible(ct) {
t.Errorf("compressible(%q) = true", ct)
}
}
}