// Package middleware provides chi-compatible HTTP middleware: // request-id, panic recovery, structured logging, CORS. package middleware import ( "context" "net/http" "runtime/debug" "strings" "time" "github.com/google/uuid" "go.uber.org/zap" ) type ctxKey string const ctxKeyRequestID ctxKey = "request_id" // RequestID assigns a uuid v4 to each request and stores it in context. func RequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("X-Request-Id") if id == "" { id = uuid.NewString() } ctx := context.WithValue(r.Context(), ctxKeyRequestID, id) w.Header().Set("X-Request-Id", id) next.ServeHTTP(w, r.WithContext(ctx)) }) } func RequestIDFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxKeyRequestID).(string) return v } // Recover handles panics so a buggy handler can't take down the server. func Recover(log *zap.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { log.Error("panic in handler", zap.Any("panic", rec), zap.String("path", r.URL.Path), zap.ByteString("stack", debug.Stack())) http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } } // Logger logs one structured line per request. func Logger(log *zap.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rw := &statusRecorder{ResponseWriter: w, status: 200} next.ServeHTTP(rw, r) log.Info("http", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Int("status", rw.status), zap.Int64("duration_ms", time.Since(start).Milliseconds()), zap.String("request_id", RequestIDFromCtx(r.Context())), zap.String("ip", clientIP(r))) }) } } // CORS returns a permissive CORS handler. The Analytics console calls the API // directly from the browser during development. func CORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-Id") w.Header().Set("Access-Control-Max-Age", "86400") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func clientIP(r *http.Request) string { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { if i := strings.Index(xff, ","); i >= 0 { return strings.TrimSpace(xff[:i]) } return strings.TrimSpace(xff) } if rip := r.Header.Get("X-Real-Ip"); rip != "" { return rip } return r.RemoteAddr } type statusRecorder struct { http.ResponseWriter status int } func (s *statusRecorder) WriteHeader(code int) { s.status = code s.ResponseWriter.WriteHeader(code) }