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 | } | ||