add realip, requestlog, recoverer middlewares
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>
This commit is contained in:
parent
9dc0fc5d26
commit
b26ef7439e
10 changed files with 624 additions and 0 deletions
36
recoverer/recoverer.go
Normal file
36
recoverer/recoverer.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Package recoverer catches panics inside HTTP handlers, logs them with stack
|
||||
// trace, and writes a 500 response.
|
||||
package recoverer
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"git.juancwu.dev/juancwu/errx"
|
||||
)
|
||||
|
||||
const op = "recoverer"
|
||||
|
||||
// New returns a panic-recovery middleware.
|
||||
func New() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
rec := recover()
|
||||
if rec == nil {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if e, ok := rec.(error); ok {
|
||||
err = errx.Wrap(op, e)
|
||||
} else {
|
||||
err = errx.Newf(op, "panic: %v", rec)
|
||||
}
|
||||
log.Printf("%v\n%s", err, debug.Stack())
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
70
recoverer/recoverer_test.go
Normal file
70
recoverer/recoverer_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue