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