package httpapi import ( "context" "crypto/rand" "encoding/hex" "log/slog" "net/http" "runtime/debug" "github.com/mitbringsl/backend/internal/config" ) // ctxKey is an unexported key type for context values in this package. type ctxKey int const ( ctxKeyRequestID ctxKey = iota ctxKeyUserID ctxKeySessionID ) type wrappedWriter struct { http.ResponseWriter status int bytes int } func (w *wrappedWriter) WriteHeader(status int) { w.status = status w.ResponseWriter.WriteHeader(status) } func (w *wrappedWriter) Write(b []byte) (int, error) { if w.status == 0 { w.status = http.StatusOK } n, err := w.ResponseWriter.Write(b) w.bytes += n return n, err } // requestIDMiddleware injects a random request id into the request context and response header. func requestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("X-Request-ID") if id == "" { b := make([]byte, 8) _, _ = rand.Read(b) id = hex.EncodeToString(b) } w.Header().Set("X-Request-ID", id) ctx := context.WithValue(r.Context(), ctxKeyRequestID, id) next.ServeHTTP(w, r.WithContext(ctx)) }) } // loggingMiddleware logs each request as structured JSON. func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ww := &wrappedWriter{ResponseWriter: w} next.ServeHTTP(ww, r) slog.Info("http request", "method", r.Method, "path", r.URL.Path, "status", ww.status, "bytes", ww.bytes, "request_id", r.Context().Value(ctxKeyRequestID), "remote", r.RemoteAddr, ) }) } // recoverMiddleware catches panics, logs them and returns 500. func recoverMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { slog.Error("panic recovered", "error", rec, "request_id", r.Context().Value(ctxKeyRequestID), "stack", string(debug.Stack()), ) renderError(w, http.StatusInternalServerError, "Internal error", "An unexpected error occurred.") } }() next.ServeHTTP(w, r) }) } // corsMiddleware adds permissive-but-scoped CORS headers when configured. // Native clients (Android) don't need CORS; this only affects browsers. func corsMiddleware(cfg *config.Config) func(http.Handler) http.Handler { allowed := map[string]bool{} for _, o := range cfg.CORSAllowedOrigins { if o != "" { allowed[o] = true } } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" && allowed[origin] { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Vary", "Origin") w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } } next.ServeHTTP(w, r) }) } } // Chain wires middlewares in declaration order: first one runs outermost. func Chain(h http.Handler, mws ...func(http.Handler) http.Handler) http.Handler { for i := len(mws) - 1; i >= 0; i-- { h = mws[i](h) } return h }