package middleware import ( "io" "log/slog" "net/http" "testing" ) // nullWriter is a ResponseWriter that keeps nothing: httptest.NewRecorder grows // a buffer per request and this measurement is of the middleware. type nullWriter struct{ header http.Header } func (w *nullWriter) Header() http.Header { return w.header } func (w *nullWriter) Write(b []byte) (int, error) { return len(b), nil } func (w *nullWriter) WriteHeader(int) {} // discardLog silences the default logger for one benchmark. The panic case // below logs a stack per iteration, and without this the run would be measuring // the terminal as much as the middleware — and would bury the result under it. func discardLog(b *testing.B) { b.Helper() previous := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{ Level: slog.LevelDebug, }))) b.Cleanup(func() { slog.SetDefault(previous) }) } // benchRequest is the request every case below serves. chi and net/http do not // mutate it — a router that needs a context puts the new one on a copy — so one // value is safe to reuse across iterations, and building it per iteration would // measure httptest.NewRequest. func benchRequest(b *testing.B) *http.Request { b.Helper() r, err := http.NewRequest(http.MethodGet, "http://bench.example.org/tokens", nil) if err != nil { b.Fatalf("building the request: %v", err) } return r } // BenchmarkPrivateCache is two header writes in front of every response on the // instance — the cheapest middleware here and the one on the most paths. func BenchmarkPrivateCache(b *testing.B) { h := PrivateCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) r := benchRequest(b) b.ReportAllocs() for b.Loop() { h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r) } } // BenchmarkRecoverPanics measures both halves of the guard. // // "clean" is what every request that does not panic pays: one wrapper // allocation, one deferred recover. It is the number that matters, because it // is charged to the whole surface for the benefit of the rare request below. // // "panicking" is the rendered 500, stack capture and log record included. It is // slow on purpose — debug.Stack() walks the goroutine — and it is here so that // a change which makes an already bad minute worse is visible. func BenchmarkRecoverPanics(b *testing.B) { discardLog(b) r := benchRequest(b) b.Run("clean", func(b *testing.B) { h := RecoverPanics(renderInternal)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) b.ReportAllocs() for b.Loop() { h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r) } }) b.Run("panicking", func(b *testing.B) { h := RecoverPanics(renderInternal)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { panic("the store is not reachable") })) b.ReportAllocs() for b.Loop() { h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r) } }) } // BenchmarkChain is the two middlewares as a service installs them, one inside // the other, so that the sum is measured rather than inferred from the two // numbers above — the wrapper allocations compose and the header writes do not. func BenchmarkChain(b *testing.B) { discardLog(b) h := PrivateCache(RecoverPanics(renderInternal)( http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))) r := benchRequest(b) b.ReportAllocs() for b.Loop() { h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r) } }