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.go → internal/compress/compress.go +32−14

@@ -1,6 +1,12 @@
1 package web
1 package compress
2 2
3 // gzip response compression.
3 // Package compress gzips text responses.
4 //
5 // It is shared by the two HTTP surfaces that serve bytes: the forge UI (wrapped
6 // into its middleware, because it needs the writer inside its own panic and
7 // page-view bookkeeping) and hosted sites (wrapped once around the site
8 // dispatcher). One implementation so both answer the same negotiation and the
9 // same header rules.
4 10 //
5 11 // The web surface sends HTML, CSS, JavaScript and source blobs, all of which
6 12 // compress to a fraction of their size: the vendored Prism bundle alone is
@@ -33,9 +39,9 @@ import (
33 39 // trailer. Below it the compressed form is usually larger than the source.
34 40 const compressMinBytes = 256
35 41
36 // compressor decides per response whether to gzip, and lazily starts the stream
37 // on the first write so a header-only response never opens one.
38 type compressor struct {
42 // Writer decides per response whether to gzip, and lazily starts the stream on
43 // the first write so a header-only response never opens one.
44 type Writer struct {
39 45 rw http.ResponseWriter
40 46 r *http.Request
41 47 gz *gzip.Writer
@@ -44,13 +50,25 @@ type compressor struct {
44 50 gzipping bool
45 51 }
46 52
47 func newCompressor(rw http.ResponseWriter, r *http.Request) *compressor {
48 return &compressor{rw: rw, r: r}
53 // New wraps rw for one request. The caller must Close it when the handler has
54 // returned; Handler does that for a whole handler tree.
55 func New(rw http.ResponseWriter, r *http.Request) *Writer {
56 return &Writer{rw: rw, r: r}
57 }
58
59 // Handler wraps next so every response it writes is compressed by the rules in
60 // New. It is the whole of the site surface's compression support.
61 func Handler(next http.Handler) http.Handler {
62 return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
63 w := New(rw, r)
64 next.ServeHTTP(w, r)
65 w.Close()
66 })
49 67 }
50 68
51 func (c *compressor) Header() http.Header { return c.rw.Header() }
69 func (c *Writer) Header() http.Header { return c.rw.Header() }
52 70
53 func (c *compressor) WriteHeader(status int) {
71 func (c *Writer) WriteHeader(status int) {
54 72 if c.wroteHeader {
55 73 return
56 74 }
@@ -66,7 +84,7 @@ func (c *compressor) WriteHeader(status int) {
66 84 c.rw.WriteHeader(status)
67 85 }
68 86
69 func (c *compressor) Write(b []byte) (int, error) {
87 func (c *Writer) Write(b []byte) (int, error) {
70 88 if !c.wroteHeader {
71 89 c.WriteHeader(http.StatusOK)
72 90 }
@@ -81,7 +99,7 @@ func (c *compressor) Write(b []byte) (int, error) {
81 99
82 100 // Flush keeps streaming handlers working: the header is settled, the gzip
83 101 // buffer is emptied, and the underlying writer is flushed if it can be.
84 func (c *compressor) Flush() {
102 func (c *Writer) Flush() {
85 103 if !c.wroteHeader {
86 104 c.WriteHeader(http.StatusOK)
87 105 }
@@ -96,18 +114,18 @@ func (c *compressor) Flush() {
96 114 // Close finishes the gzip stream. It is called once by the middleware after the
97 115 // handler returns, never by a handler, but it is idempotent enough for the
98 116 // panic path that may have written nothing at all.
99 func (c *compressor) Close() {
117 func (c *Writer) Close() {
100 118 if c.gz != nil {
101 119 _ = c.gz.Close()
102 120 c.gz = nil
103 121 }
104 122 }
105 123
106 func (c *compressor) Unwrap() http.ResponseWriter { return c.rw }
124 func (c *Writer) Unwrap() http.ResponseWriter { return c.rw }
107 125
108 126 // acceptsGzip reports whether the client asked for gzip and would accept it:
109 127 // `gzip` with a q-value of zero is a refusal, not a request.
110 func (c *compressor) acceptsGzip() bool {
128 func (c *Writer) acceptsGzip() bool {
111 129 if c.r.Method == http.MethodHead {
112 130 return false
113 131 }