Initial implementation of lightmux-contrib, a sibling module to lightmux that hosts opinionated middlewares with one sub-package per middleware: - realip: resolves the originating client IP from CF-Connecting-IP, True-Client-IP, X-Real-IP, or X-Forwarded-For. Optional peer-CIDR allowlist via netip.Prefix. - requestlog: emits a structured http.request record (method, path, status, duration, client) per request via splinter. - recoverer: catches panics, wraps with errx under op "recoverer", logs with stack, and writes a 500 response. Each package exposes a single New(...) constructor returning func(http.Handler) http.Handler. The contrib module intentionally does not import lightmux — middlewares interoperate via the standard stdlib middleware shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
1.6 KiB
Go
70 lines
1.6 KiB
Go
package recoverer
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func captureLog(t *testing.T) *bytes.Buffer {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
orig := log.Default().Writer()
|
|
log.Default().SetOutput(&buf)
|
|
t.Cleanup(func() { log.Default().SetOutput(orig) })
|
|
return &buf
|
|
}
|
|
|
|
func TestNewCatchesStringPanic(t *testing.T) {
|
|
buf := captureLog(t)
|
|
|
|
h := New()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
panic("boom")
|
|
}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
|
|
if rr.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want 500", rr.Code)
|
|
}
|
|
out := buf.String()
|
|
for _, want := range []string{"recoverer", "panic: boom"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("log missing %q\nfull: %s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewWrapsErrorPanic(t *testing.T) {
|
|
buf := captureLog(t)
|
|
|
|
cause := errors.New("db down")
|
|
h := New()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
panic(cause)
|
|
}))
|
|
|
|
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
|
|
|
|
out := buf.String()
|
|
if !strings.Contains(out, "recoverer: db down") {
|
|
t.Errorf("expected errx-wrapped breadcrumb, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestNewPassesThrough(t *testing.T) {
|
|
called := false
|
|
h := New()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
rr := httptest.NewRecorder()
|
|
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
if !called || rr.Code != http.StatusOK {
|
|
t.Errorf("non-panic path broken: called=%v code=%d", called, rr.Code)
|
|
}
|
|
}
|