package api import ( "bufio" "errors" "log/slog" "net" "net/http" "time" ) type statusRecorder struct { http.ResponseWriter status int bytes int } func (s *statusRecorder) WriteHeader(code int) { s.status = code s.ResponseWriter.WriteHeader(code) } func (s *statusRecorder) Write(b []byte) (int, error) { if s.status == 0 { s.status = http.StatusOK } n, err := s.ResponseWriter.Write(b) s.bytes += n return n, err } // Hijack passes through to the underlying ResponseWriter so WebSocket // upgrades work despite the middleware wrapper. Returning ErrNotSupported // (rather than a custom error) lets callers detect this generically. func (s *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { if h, ok := s.ResponseWriter.(http.Hijacker); ok { return h.Hijack() } return nil, nil, errors.New("response writer does not support hijack") } func (s *statusRecorder) Flush() { if f, ok := s.ResponseWriter.(http.Flusher); ok { f.Flush() } } func loggingMiddleware(logger *slog.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() rec := &statusRecorder{ResponseWriter: w} next.ServeHTTP(rec, r) logger.Info("http", "method", r.Method, "path", r.URL.Path, "status", rec.status, "bytes", rec.bytes, "duration_ms", time.Since(start).Milliseconds(), "remote", r.RemoteAddr, ) }) } } func recoverMiddleware(logger *slog.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 { logger.Error("panic", "err", rec, "path", r.URL.Path) writeError(w, http.StatusInternalServerError, "internal server error") } }() next.ServeHTTP(w, r) }) } }