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.

Code

394 commit(s) on main

Default the run verbs to origin Keenan Nemetz · 5m ago 2e6aa96e
history blame raw 6.5 KiB
1package compress
2
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.
10//
11// The web surface sends HTML, CSS, JavaScript and source blobs, all of which
12// compress to a fraction of their size: the vendored Prism bundle alone is
13// 545 KB and lands near 200 KB gzipped, and a rendered page about a fifth of its
14// size. There is no brotli here on purpose — it would add a dependency for a few
15// points of ratio, and Go's gzip needs none.
16//
17// The rules are narrow, because the same handler chain also streams git
18// responses, release archives and LFS objects:
19//
20// - only when the client sent `Accept-Encoding: gzip` with q > 0;
21// - only text-like content types, never a body that is already encoded;
22// - never a Range request (the compressed body has no meaningful ranges);
23// - never a body the handler has already sized below compressMinBytes;
24// - never a 1xx, 204, 206, 304 or redirect (nothing to gain, and 304 must
25// stay bodiless).
26//
27// The writer is transparent otherwise: it passes Header through, keeps Flush
28// working for the streaming handlers, and exposes Unwrap so a handler can reach
29// the underlying ResponseWriter.
30
31import (
32 "compress/gzip"
33 "net/http"
34 "strconv"
35 "strings"
36)
37
38// compressMinBytes is the smallest declared body worth a gzip header and
39// trailer. Below it the compressed form is usually larger than the source.
40const compressMinBytes = 256
41
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.
44type Writer struct {
45 rw http.ResponseWriter
46 r *http.Request
47 gz *gzip.Writer
48 status int
49 wroteHeader bool
50 gzipping bool
51}
52
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.
55func 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.
61func 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 })
67}
68
69func (c *Writer) Header() http.Header { return c.rw.Header() }
70
71func (c *Writer) WriteHeader(status int) {
72 if c.wroteHeader {
73 return
74 }
75 c.wroteHeader = true
76 c.status = status
77 h := c.rw.Header()
78 if c.acceptsGzip() && compressibleStatus(status) && compressibleHeader(h, c.r) {
79 c.gzipping = true
80 h.Set("Content-Encoding", "gzip")
81 h.Del("Content-Length")
82 h.Add("Vary", "Accept-Encoding")
83 }
84 c.rw.WriteHeader(status)
85}
86
87func (c *Writer) Write(b []byte) (int, error) {
88 if !c.wroteHeader {
89 c.WriteHeader(http.StatusOK)
90 }
91 if !c.gzipping {
92 return c.rw.Write(b)
93 }
94 if c.gz == nil {
95 c.gz = gzip.NewWriter(c.rw)
96 }
97 return c.gz.Write(b)
98}
99
100// Flush keeps streaming handlers working: the header is settled, the gzip
101// buffer is emptied, and the underlying writer is flushed if it can be.
102func (c *Writer) Flush() {
103 if !c.wroteHeader {
104 c.WriteHeader(http.StatusOK)
105 }
106 if c.gz != nil {
107 _ = c.gz.Flush()
108 }
109 if f, ok := c.rw.(http.Flusher); ok {
110 f.Flush()
111 }
112}
113
114// Close finishes the gzip stream. It is called once by the middleware after the
115// handler returns, never by a handler, but it is idempotent enough for the
116// panic path that may have written nothing at all.
117func (c *Writer) Close() {
118 if c.gz != nil {
119 _ = c.gz.Close()
120 c.gz = nil
121 }
122}
123
124func (c *Writer) Unwrap() http.ResponseWriter { return c.rw }
125
126// acceptsGzip reports whether the client asked for gzip and would accept it:
127// `gzip` with a q-value of zero is a refusal, not a request.
128func (c *Writer) acceptsGzip() bool {
129 if c.r.Method == http.MethodHead {
130 return false
131 }
132 for _, part := range strings.Split(c.r.Header.Get("Accept-Encoding"), ",") {
133 name, params, _ := strings.Cut(strings.TrimSpace(part), ";")
134 if !strings.EqualFold(strings.TrimSpace(name), "gzip") {
135 continue
136 }
137 q := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(params), "q="))
138 if q == "" {
139 return true
140 }
141 if v, err := strconv.ParseFloat(q, 64); err == nil {
142 return v > 0
143 }
144 return true
145 }
146 return false
147}
148
149// compressibleStatus covers the bodies worth compressing: a rendered page, a
150// fragment, and the styled error page. Redirects are a sentence, a 304 must stay
151// bodiless, and a 206 is already a byte range.
152func compressibleStatus(status int) bool {
153 switch status {
154 case http.StatusNoContent, http.StatusNotModified, http.StatusPartialContent:
155 return false
156 }
157 return (status >= 200 && status < 300) || status >= 400
158}
159
160// compressibleHeader applies the remaining rules: a body that is already
161// encoded stays as it is, a Range request is answered with bytes, and a body
162// the handler already sized as tiny is not worth compressing.
163func compressibleHeader(h http.Header, r *http.Request) bool {
164 if h.Get("Content-Encoding") != "" || h.Get("Content-Range") != "" {
165 return false
166 }
167 // A handler that promised exact bytes (a raw download with its Content-Length)
168 // opts out with the standard directive rather than a private header.
169 if strings.Contains(strings.ToLower(h.Get("Cache-Control")), "no-transform") {
170 return false
171 }
172 if r.Header.Get("Range") != "" {
173 return false
174 }
175 if n, err := strconv.Atoi(h.Get("Content-Length")); err == nil && n < compressMinBytes {
176 return false
177 }
178 return compressibleType(h.Get("Content-Type"))
179}
180
181// compressibleType is the allow-list of types worth gzipping. Anything else —
182// images, archives, packs, LFS objects, git protocol output — is already
183// compressed or is a byte range where compression cannot help.
184func compressibleType(contentType string) bool {
185 ct := strings.ToLower(strings.TrimSpace(contentType))
186 if i := strings.IndexByte(ct, ';'); i >= 0 {
187 ct = strings.TrimSpace(ct[:i])
188 }
189 switch ct {
190 case "text/event-stream":
191 return false
192 case "image/svg+xml",
193 "application/json",
194 "application/javascript",
195 "application/x-javascript",
196 "application/xml",
197 "application/xhtml+xml",
198 "application/manifest+json",
199 "application/rss+xml",
200 "application/atom+xml":
201 return true
202 }
203 return strings.HasPrefix(ct, "text/") ||
204 strings.HasSuffix(ct, "+json") ||
205 strings.HasSuffix(ct, "+xml")
206}
207