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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0: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/compress_test.go +64−3

@@ -1,9 +1,11 @@
1 1 package server_test
2 2
3 3 import (
4 "bytes"
4 5 "compress/gzip"
5 6 "io"
6 7 "net/http"
8 "strconv"
7 9 "strings"
8 10 "testing"
9 11 )
@@ -59,10 +61,16 @@ func TestPagesAreCompressedWhenTheClientAsks(t *testing.T) {
59 61 if !strings.Contains(strings.Join(resp.Header.Values("Vary"), ","), "Accept-Encoding") {
60 62 t.Fatalf("Vary does not name Accept-Encoding: %v", resp.Header.Values("Vary"))
61 63 }
62 if got := resp.Header.Get("Content-Length"); got != "" {
63 t.Fatalf("compressed page announced Content-Length %q", got)
64 compressed, err := io.ReadAll(resp.Body)
65 if err != nil {
66 t.Fatal(err)
67 }
68 // net/http may add a length for a small buffered response; if it does, it is
69 // the length of the gzipped bytes, never of the page the handler wrote.
70 if got := resp.Header.Get("Content-Length"); got != "" && got != strconv.Itoa(len(compressed)) {
71 t.Fatalf("Content-Length = %q on a %d-byte compressed body", got, len(compressed))
64 72 }
65 if page := gunzip(t, resp.Body); !strings.Contains(page, `id="file-tree"`) {
73 if page := gunzip(t, bytes.NewReader(compressed)); !strings.Contains(page, `id="file-tree"`) {
66 74 t.Fatalf("decompressed page is not the code browser:\n%s", page[:min(len(page), 300)])
67 75 }
68 76 }
@@ -153,3 +161,56 @@ func TestStaticServesOnlyTheEmbeddedSet(t *testing.T) {
153 161 }
154 162 }
155 163 }
164
165 // TestSiteResponsesAreCompressed: a hosted site is a static file server, the best
166 // case gzip has, and it reaches HTTP by a different path than the forge UI (a Host
167 // match in server.Handler rather than Web.Middleware). Compressing only the forge
168 // would have left every site sending raw HTML and CSS.
169 func TestSiteResponsesAreCompressed(t *testing.T) {
170 t.Parallel()
171 f := pagesFixture(t)
172 f.publish(f.repo)
173 f.pushPages(t, map[string]string{
174 "index.html": "<!doctype html><h1>hello site</h1>\n" + strings.Repeat("<p>a paragraph</p>\n", 60),
175 "assets/style.css": "body { color: #123 }\n",
176 })
177 host := f.repo + "." + pagesDomain
178
179 code, body, hdr := f.siteGet(host, "/", [2]string{"Accept-Encoding", "gzip"})
180 if code != http.StatusOK {
181 t.Fatalf("site page = %d", code)
182 }
183 if got := hdr.Get("Content-Encoding"); got != "gzip" {
184 t.Fatalf("Content-Encoding = %q, want gzip", got)
185 }
186 // A length here is the gzipped length, not the site file's.
187 if got := hdr.Get("Content-Length"); got != "" && got != strconv.Itoa(len(body)) {
188 t.Fatalf("Content-Length = %q on a %d-byte compressed body", got, len(body))
189 }
190 if !strings.Contains(strings.Join(hdr.Values("Vary"), ","), "Accept-Encoding") {
191 t.Fatalf("Vary does not name Accept-Encoding: %v", hdr.Values("Vary"))
192 }
193 // The site's own validators survive: a push still invalidates an exact blob sha.
194 if hdr.Get("ETag") == "" || hdr.Get("Last-Modified") == "" {
195 t.Fatalf("missing validators: ETag=%q Last-Modified=%q", hdr.Get("ETag"), hdr.Get("Last-Modified"))
196 }
197 if page := gunzip(t, strings.NewReader(body)); !strings.Contains(page, "hello site") {
198 t.Fatalf("decompressed site page is not the page:\n%s", page[:min(len(page), 200)])
199 }
200
201 // A client that does not ask keeps the byte-for-byte body and its length.
202 code, plain, hdr := f.siteGet(host, "/", [2]string{"Accept-Encoding", "identity"})
203 if code != http.StatusOK || hdr.Get("Content-Encoding") != "" {
204 t.Fatalf("identity site page = %d %q", code, hdr.Get("Content-Encoding"))
205 }
206 if got, want := hdr.Get("Content-Length"), strconv.Itoa(len(plain)); got != want {
207 t.Fatalf("identity Content-Length = %q, want %q", got, want)
208 }
209 etag := hdr.Get("ETag")
210
211 // Revalidation is unaffected by the encoding: the 304 has no body to compress.
212 code, _, hdr = f.siteGet(host, "/", [2]string{"Accept-Encoding", "gzip"}, [2]string{"If-None-Match", etag})
213 if code != http.StatusNotModified || hdr.Get("Content-Encoding") != "" {
214 t.Fatalf("site revalidation = %d %q", code, hdr.Get("Content-Encoding"))
215 }
216 }