Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Compress hosted site responses too

a76b2f38cb6692f7fe88c83e4552205d398f30df · Keenan Nemetz · 12d ago · parent 306f36fc

Compress hosted site responses too

The gzip middleware covered the forge UI and nothing else: server.Handler
dispatches a site host to server/pages before the forge middleware runs, so
every hosted site was serving raw HTML, CSS and JavaScript. A site is a static
file server, which is the best case gzip has.

The compressor moves from server/web to internal/compress so both surfaces share
one implementation of the negotiation and header rules, with two entry points:
New for the forge, which needs the writer inside its own status recorder and
panic handling, and Handler for the site dispatcher.

Site responses keep their ETag and Last-Modified, so revalidation is still a 304
off the blob sha; Vary: Accept-Encoding names the representation; a 304 and a
301 carry no body to compress; and a client that does not ask for gzip still
gets the exact bytes and the exact Content-Length.

Measured on the site path, an HTML page with repeated markup goes from 1.5 KB to
83 bytes.

server/web/compress_test.go → internal/compress/compress_test.go +13−7

@@ -1,4 +1,4 @@
1 package web
1 package compress
2 2
3 3 import (
4 4 "bytes"
@@ -15,7 +15,7 @@ import (
15 15 func serveWithCompression(t *testing.T, r *http.Request, handler http.HandlerFunc) *httptest.ResponseRecorder {
16 16 t.Helper()
17 17 rec := httptest.NewRecorder()
18 c := newCompressor(rec, r)
18 c := New(rec, r)
19 19 handler(c, r)
20 20 c.Close()
21 21 return rec
@@ -144,7 +144,7 @@ func TestCompressionFlushes(t *testing.T) {
144 144 r := httptest.NewRequest(http.MethodGet, "/", nil)
145 145 r.Header.Set("Accept-Encoding", "gzip")
146 146 rec := httptest.NewRecorder()
147 c := newCompressor(rec, r)
147 c := New(rec, r)
148 148 handler := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
149 149 rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
150 150 _, _ = io.WriteString(rw, "first\n")
@@ -169,14 +169,16 @@ func TestCompressionFlushes(t *testing.T) {
169 169 }
170 170 }
171 171
172 // TestCompressorUnwraps: http.ResponseController reaches the real writer
172 // TestWriterUnwraps: http.ResponseController reaches the real writer
173 173 // through Unwrap, so nothing in the chain needs its own accessor.
174 func TestCompressorUnwraps(t *testing.T) {
174 func TestWriterUnwraps(t *testing.T) {
175 175 t.Parallel()
176 176 r := httptest.NewRequest(http.MethodGet, "/", nil)
177 177 rec := httptest.NewRecorder()
178 inner := &statusWriter{ResponseWriter: rec}
179 c := newCompressor(inner, r)
178 // One transparent wrapper, as the forge UI puts a status recorder between the
179 // handler and the compressor.
180 inner := wrapped{rec}
181 c := New(inner, r)
180 182 if got := c.Unwrap(); got != http.ResponseWriter(inner) {
181 183 t.Fatalf("Unwrap = %T, want the wrapped writer", got)
182 184 }
@@ -185,3 +187,7 @@ func TestCompressorUnwraps(t *testing.T) {
185 187 t.Fatalf("ResponseController.Flush through the compressor: %v", err)
186 188 }
187 189 }
190
191 // wrapped is the shape of every writer between a handler and the compressor:
192 // a ResponseWriter that forwards Header, Write and WriteHeader.
193 type wrapped struct{ http.ResponseWriter }