| 1 |
|
// Package middleware is the shared HTTP middleware for the custom services of a |
| 2 |
|
// self-hosted SourceHut instance (compare, spec, dolt, cover, bench, ...). |
| 3 |
|
// |
| 4 |
|
// Three things every one of those services needs, and every one of them had |
| 5 |
|
// copied byte for byte into its own web/router.go: the cache policy that keeps a |
| 6 |
|
// page rendered behind a login cookie out of every cache that is not the |
| 7 |
|
// viewer's own, the panic guard that turns a bug in a handler into the service's |
| 8 |
|
// own error page instead of a dropped connection, and the 499 that separates a |
| 9 |
|
// viewer who closed the tab from a server that broke. The copies were identical |
| 10 |
|
// down to their comments, which is the sign that they were never three |
| 11 |
|
// decisions — they were one, made once and pasted twice. This package is the one |
| 12 |
|
// copy. |
| 13 |
|
// |
| 14 |
|
// Usage, in the order the donors install them: |
| 15 |
|
// |
| 16 |
|
// r.Use(middleware.RecoverPanics(func(w http.ResponseWriter, r *http.Request, recovered any) { |
| 17 |
|
// s.renderError(w, r, http.StatusInternalServerError, internalMessage) |
| 18 |
|
// })) |
| 19 |
|
// r.Use(middleware.PrivateCache) |
| 20 |
|
// |
| 21 |
|
// RecoverPanics goes outermost so that it covers every later middleware as well |
| 22 |
|
// as the handlers; PrivateCache goes inside it so that the error page it renders |
| 23 |
|
// carries the same headers as any other answer. |
| 24 |
|
// |
| 25 |
|
// What deliberately did not move here is the donors' getHead helper. It is three |
| 26 |
|
// lines, and all three are chi's — it takes a chi.Router and calls Get and Head |
| 27 |
|
// on it — so hoisting it would put a router dependency in a package whose whole |
| 28 |
|
// point is that it needs nothing but net/http. It lives in the sibling package |
| 29 |
|
// chimw instead, which is allowed the router dependency because knowing what a |
| 30 |
|
// route is is what that package is for. |
| 31 |
|
// |
| 32 |
|
// Panics are reported through slog's default logger rather than a logger this |
| 33 |
|
// package is handed. A library has no business choosing a handler: the service |
| 34 |
|
// installs its own — scribe's tinted one on this instance — with |
| 35 |
|
// slog.SetDefault at startup, and everything logged here lands in the same |
| 36 |
|
// stream, with the same masking rules, as the service's own lines. Handing a |
| 37 |
|
// *slog.Logger to RecoverPanics would buy configurability nobody wants and cost |
| 38 |
|
// every caller a parameter. |
| 39 |
|
package middleware |
| 40 |
|
|
| 41 |
|
import ( |
| 42 |
|
"bufio" |
| 43 |
|
"errors" |
| 44 |
|
"io" |
| 45 |
|
"log/slog" |
| 46 |
|
"net" |
| 47 |
|
"net/http" |
| 48 |
|
"runtime/debug" |
| 49 |
|
) |
| 50 |
|
|
| 51 |
|
// StatusClientClosedRequest is nginx's 499: the caller went away — hung up, or |
| 52 |
|
// ran out of its own deadline — before the answer was written. |
| 53 |
|
// |
| 54 |
|
// No RFC defines it, and that costs nothing, because the one certain thing about |
| 55 |
|
// this response is that nobody reads it: the context it reports on is the |
| 56 |
|
// request's own, and it ended before there was anything to send. So the code is |
| 57 |
|
// chosen for the operator rather than for the client. What matters is that it is |
| 58 |
|
// not in the 5xx range — that is the rate an alert is written against, and a |
| 59 |
|
// browser navigating away mid-render, or a CI job that pressed ^C, must not page |
| 60 |
|
// anybody — and 499 is the value the log pipelines in front of a SourceHut |
| 61 |
|
// instance already understand, because the nginx that terminates TLS for one has |
| 62 |
|
// been writing it for this exact event since long before any of these services |
| 63 |
|
// existed. |
| 64 |
|
// |
| 65 |
|
// The alternatives are each worse in their own way. 500 is a false statement |
| 66 |
|
// about the instance: nothing broke, and whoever is woken by it finds a healthy |
| 67 |
|
// service. 408 is standard but means the other half of "nobody finished" — the |
| 68 |
|
// server gave up waiting for a body still arriving, which is the server's |
| 69 |
|
// problem and is safe to retry — and several clients do retry it automatically, |
| 70 |
|
// which is the last thing to tell a caller that cancelled on purpose. 504 names |
| 71 |
|
// a gateway timing out upstream, and there is no upstream here. |
| 72 |
|
// |
| 73 |
|
// It belongs in a middleware package rather than next to one service's status |
| 74 |
|
// mapping because both surfaces of every service reach for it: the HTML side |
| 75 |
|
// when a render's context is already cancelled, the API side in its |
| 76 |
|
// error-to-status switch. |
| 77 |
|
const StatusClientClosedRequest = 499 |
| 78 |
|
|
| 79 |
|
// PrivateCache marks every answer of a surface as one no cache may reuse for |
| 80 |
|
// another viewer. |
| 81 |
|
// |
| 82 |
|
// These services render per-viewer documents at URLs that say nothing about the |
| 83 |
|
// viewer: a token list, a repository page that is a page to its owner and a 404 |
| 84 |
|
// to everyone else, a dashboard of somebody's own runs. A cache with no |
| 85 |
|
// instruction treats a 200 to a GET as reusable, so one proxy, one CDN or one |
| 86 |
|
// browser on a shared machine is all it takes for somebody to be served another |
| 87 |
|
// account's page — a disclosure arriving by a route no visibility check can |
| 88 |
|
// stand in front of. |
| 89 |
|
// |
| 90 |
|
// The policy is "private, no-store" and not merely "no-cache" because the two |
| 91 |
|
// answer different questions. no-cache still permits a *stored* copy, and only |
| 92 |
|
// requires it to be revalidated before reuse; the copy sits in the shared proxy |
| 93 |
|
// and on the disk of the shared machine either way, and a revalidation carries |
| 94 |
|
// the next viewer's cookie, not the one the page was rendered for. private bars |
| 95 |
|
// the shared caches from keeping it at all, no-store bars the private ones from |
| 96 |
|
// writing it down, and for a page whose most sensitive form contains a live |
| 97 |
|
// credential in plaintext, "do not write this to disk" is the instruction that |
| 98 |
|
// was actually meant. |
| 99 |
|
// |
| 100 |
|
// Vary names Cookie and Authorization for the same reason: they are the two |
| 101 |
|
// inputs that decide who the page is for, so any cache that does keep something |
| 102 |
|
// must at least not hand it to a request that presented different ones. |
| 103 |
|
// |
| 104 |
|
// It is a middleware and not a line in the render path because render is not the |
| 105 |
|
// only writer: /healthz is text/plain, static assets are bytes, a badge is an |
| 106 |
|
// image, and a header this important must not depend on which write path a |
| 107 |
|
// future page picks. The headers are set before the handler runs, so a handler |
| 108 |
|
// that needs different ones — the static handler, which serves immutable |
| 109 |
|
// hashed-name assets — overrides them with Set. |
| 110 |
2 |
func PrivateCache(next http.Handler) http.Handler { |
| 111 |
2 |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 112 |
2 |
SetPrivateCache(w) |
| 113 |
2 |
next.ServeHTTP(w, r) |
| 114 |
2 |
}) |
| 115 |
|
} |
| 116 |
|
|
| 117 |
|
// SetPrivateCache writes the two headers PrivateCache exists for. |
| 118 |
|
// |
| 119 |
|
// It is split out because a service also answers from places the router's |
| 120 |
|
// middleware chain does not reach: the deny path called by the authentication |
| 121 |
|
// middleware in front of the whole mount, before the router has seen the request |
| 122 |
|
// at all, is the donors' example. Those answers are as viewer-specific as any |
| 123 |
|
// page and must not be cached either. |
| 124 |
3 |
func SetPrivateCache(w http.ResponseWriter) { |
| 125 |
3 |
w.Header().Set("Cache-Control", "private, no-store") |
| 126 |
3 |
w.Header().Set("Vary", "Cookie, Authorization") |
| 127 |
3 |
} |
| 128 |
|
|
| 129 |
|
// RecoverPanics turns a panicking handler into the error page the service would |
| 130 |
|
// have written for any other bug. |
| 131 |
|
// |
| 132 |
|
// Without it a panic reaches net/http, which logs it and closes the connection |
| 133 |
|
// without a response: the viewer sees a browser error page, not the service's, |
| 134 |
|
// and an operator sees a stack with no request context around it. It is this and |
| 135 |
|
// not chi's middleware.Recoverer because that one answers with plain text, and |
| 136 |
|
// these surfaces answer with pages. |
| 137 |
|
// |
| 138 |
|
// render is the seam. Each service renders its own 500 — its own chrome, its own |
| 139 |
|
// message, its own template set — so the middleware cannot write the page, only |
| 140 |
|
// decide when one is owed. The callback takes the recovered value so that a |
| 141 |
|
// service which wants to classify the panic can, and a service which does not |
| 142 |
|
// ignores the parameter; a renderer with the donors' (w, r, status, message) |
| 143 |
|
// shape is passed as a one-line closure rather than being reshaped: |
| 144 |
|
// |
| 145 |
|
// middleware.RecoverPanics(func(w http.ResponseWriter, r *http.Request, _ any) { |
| 146 |
|
// s.renderError(w, r, http.StatusInternalServerError, internalMessage) |
| 147 |
|
// }) |
| 148 |
|
// |
| 149 |
|
// A nil render is a wiring mistake and panics here, at construction, rather than |
| 150 |
|
// at 3am inside a deferred function where the only thing left to do about it is |
| 151 |
|
// drop the connection. |
| 152 |
|
// |
| 153 |
|
// The panic value and the stack are logged here and never handed to the viewer — |
| 154 |
|
// an error from below names tables, queries and paths. Logging is the |
| 155 |
|
// middleware's job and not the callback's because the stack is only reachable |
| 156 |
|
// from inside the deferred function that recovered; a service left to log it |
| 157 |
|
// would sooner or later log the value alone, and a panic without a stack is a |
| 158 |
|
// bug report with the address torn off. |
| 159 |
|
// |
| 160 |
|
// Two panics are not this middleware's to answer. |
| 161 |
|
// |
| 162 |
|
// http.ErrAbortHandler is re-panicked, because the standard library defines it |
| 163 |
|
// as "this handler is giving up on this connection on purpose": net/http expects |
| 164 |
|
// to see it, drops the connection silently and logs nothing. Answering it with a |
| 165 |
|
// page would resurrect a response somebody deliberately abandoned. |
| 166 |
|
// |
| 167 |
|
// A panic that happens *after* the response has started is answered by dropping |
| 168 |
|
// the connection, not by rendering. This is where this package parts with its |
| 169 |
|
// donors, which called their error renderer unconditionally: with bytes already |
| 170 |
|
// on the wire that write is a superfluous WriteHeader the standard library logs |
| 171 |
|
// and ignores, followed by an error page appended to the middle of a truncated |
| 172 |
|
// one — a body that is neither document, with a 200 status line in front of it |
| 173 |
|
// claiming both are fine. Panicking with http.ErrAbortHandler instead makes the |
| 174 |
|
// failure legible: the connection dies mid-body, the client sees a short read |
| 175 |
|
// against the Content-Length it was promised (or a chunked stream with no |
| 176 |
|
// terminator) and reports a failed transfer, which is what happened. Detecting |
| 177 |
|
// this is what the response writer is wrapped for. |
| 178 |
9 |
func RecoverPanics(render func(w http.ResponseWriter, r *http.Request, recovered any)) func(http.Handler) http.Handler { |
| 179 |
9 |
if render == nil { |
| 180 |
1 |
panic("middleware: RecoverPanics needs a render callback") |
| 181 |
|
} |
| 182 |
8 |
return func(next http.Handler) http.Handler { |
| 183 |
8 |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 184 |
8 |
tracked := &startTracker{ResponseWriter: w} |
| 185 |
8 |
defer func() { |
| 186 |
8 |
recovered := recover() |
| 187 |
8 |
if recovered == nil { |
| 188 |
1 |
return |
| 189 |
1 |
} |
| 190 |
7 |
if err, ok := recovered.(error); ok && errors.Is(err, http.ErrAbortHandler) { |
| 191 |
1 |
panic(recovered) |
| 192 |
|
} |
| 193 |
6 |
slog.ErrorContext(r.Context(), "panic serving a request", |
| 194 |
6 |
"method", r.Method, |
| 195 |
6 |
"path", r.URL.Path, |
| 196 |
6 |
"panic", recovered, |
| 197 |
6 |
"stack", string(debug.Stack())) |
| 198 |
6 |
if tracked.started { |
| 199 |
2 |
// Half a page is already out. There is no status line left |
| 200 |
2 |
// to send and nothing useful to append; abandon the |
| 201 |
2 |
// connection instead of corrupting the body further. |
| 202 |
2 |
panic(http.ErrAbortHandler) |
| 203 |
|
} |
| 204 |
4 |
renderOnce(render, tracked, r, recovered) |
| 205 |
|
}() |
| 206 |
8 |
next.ServeHTTP(tracked, r) |
| 207 |
|
}) |
| 208 |
|
} |
| 209 |
|
} |
| 210 |
|
|
| 211 |
|
// renderOnce calls render under a guard of its own, so that a panic while |
| 212 |
|
// rendering the error page cannot be fed back into the path that renders error |
| 213 |
|
// pages. |
| 214 |
|
// |
| 215 |
|
// The second failure is logged and the connection dropped. It is deliberately |
| 216 |
|
// not a second attempt at a page: whatever is broken — a template, the chrome, |
| 217 |
|
// the store the chrome reads a username from — is exactly what the retry would |
| 218 |
|
// use, and the third try would be no different from the second. One log line |
| 219 |
|
// naming both panics is what an operator needs; a loop is what they would |
| 220 |
|
// otherwise get. |
| 221 |
|
func renderOnce( |
| 222 |
|
render func(w http.ResponseWriter, r *http.Request, recovered any), |
| 223 |
|
w http.ResponseWriter, |
| 224 |
|
r *http.Request, |
| 225 |
|
recovered any, |
| 226 |
4 |
) { |
| 227 |
4 |
defer func() { |
| 228 |
4 |
second := recover() |
| 229 |
4 |
if second == nil { |
| 230 |
2 |
return |
| 231 |
2 |
} |
| 232 |
2 |
if err, ok := second.(error); ok && errors.Is(err, http.ErrAbortHandler) { |
| 233 |
1 |
panic(second) |
| 234 |
|
} |
| 235 |
1 |
slog.ErrorContext(r.Context(), "panic rendering the error page", |
| 236 |
1 |
"method", r.Method, |
| 237 |
1 |
"path", r.URL.Path, |
| 238 |
1 |
"panic", second, |
| 239 |
1 |
"original_panic", recovered, |
| 240 |
1 |
"stack", string(debug.Stack())) |
| 241 |
1 |
panic(http.ErrAbortHandler) |
| 242 |
|
}() |
| 243 |
4 |
render(w, r, recovered) |
| 244 |
|
} |
| 245 |
|
|
| 246 |
|
// startTracker records whether anything has reached the wire yet. |
| 247 |
|
// |
| 248 |
|
// "The response has started" is the one fact RecoverPanics needs and net/http |
| 249 |
|
// does not expose: by the time a panic is recovered, the only way to know |
| 250 |
|
// whether a status line has already gone out is to have watched for it. Every |
| 251 |
|
// method that commits the response — an explicit WriteHeader, the implicit one |
| 252 |
|
// inside the first Write, a Flush, a Hijack — sets the flag before delegating. |
| 253 |
|
// |
| 254 |
|
// Wrapping a ResponseWriter costs the concrete type behind it, so the methods a |
| 255 |
|
// handler may reasonably reach for are carried across: |
| 256 |
|
// |
| 257 |
|
// - Unwrap is the net/http convention (Go 1.20+) that lets an |
| 258 |
|
// http.ResponseController find the real writer, which is how deadlines and |
| 259 |
|
// flushes are meant to be reached through wrappers like this one. |
| 260 |
|
// - Flush and Hijack are implemented directly as well, because plenty of code |
| 261 |
|
// still type-asserts for http.Flusher and http.Hijacker rather than going |
| 262 |
|
// through the controller. They delegate through the controller, which |
| 263 |
|
// returns http.ErrNotSupported if the writer underneath genuinely cannot do |
| 264 |
|
// it — the same outcome as the assertion having failed, minus the silence. |
| 265 |
|
// - ReadFrom keeps http.ServeContent and io.Copy on the fast path: net/http's |
| 266 |
|
// own writer implements io.ReaderFrom, and losing it would turn every static |
| 267 |
|
// asset into a buffered copy loop. |
| 268 |
|
type startTracker struct { |
| 269 |
|
http.ResponseWriter |
| 270 |
|
started bool |
| 271 |
|
} |
| 272 |
|
|
| 273 |
4 |
func (t *startTracker) WriteHeader(status int) { |
| 274 |
4 |
t.started = true |
| 275 |
4 |
t.ResponseWriter.WriteHeader(status) |
| 276 |
4 |
} |
| 277 |
|
|
| 278 |
4 |
func (t *startTracker) Write(b []byte) (int, error) { |
| 279 |
4 |
t.started = true |
| 280 |
4 |
return t.ResponseWriter.Write(b) |
| 281 |
4 |
} |
| 282 |
|
|
| 283 |
|
// Unwrap gives http.ResponseController the writer this one wraps. |
| 284 |
0 |
func (t *startTracker) Unwrap() http.ResponseWriter { |
| 285 |
0 |
return t.ResponseWriter |
| 286 |
0 |
} |
| 287 |
|
|
| 288 |
|
// Flush commits whatever is buffered, which starts the response. |
| 289 |
1 |
func (t *startTracker) Flush() { |
| 290 |
1 |
t.started = true |
| 291 |
1 |
// The error is the writer saying it cannot flush, which is what an |
| 292 |
1 |
// unsatisfied http.Flusher assertion would have said by not existing. |
| 293 |
1 |
_ = http.NewResponseController(t.ResponseWriter).Flush() |
| 294 |
1 |
} |
| 295 |
|
|
| 296 |
|
// Hijack hands the connection to the caller, after which nothing here can write |
| 297 |
|
// a status line — so the response counts as started. |
| 298 |
0 |
func (t *startTracker) Hijack() (net.Conn, *bufio.ReadWriter, error) { |
| 299 |
0 |
t.started = true |
| 300 |
0 |
return http.NewResponseController(t.ResponseWriter).Hijack() |
| 301 |
0 |
} |
| 302 |
|
|
| 303 |
|
// ReadFrom preserves the io.ReaderFrom fast path of the writer underneath. |
| 304 |
0 |
func (t *startTracker) ReadFrom(src io.Reader) (int64, error) { |
| 305 |
0 |
t.started = true |
| 306 |
0 |
if rf, ok := t.ResponseWriter.(io.ReaderFrom); ok { |
| 307 |
0 |
return rf.ReadFrom(src) |
| 308 |
0 |
} |
| 309 |
|
// Copy to the wrapped writer and not to t, or this is a recursion. |
| 310 |
0 |
return io.Copy(t.ResponseWriter, src) |
| 311 |
|
} |