package api import ( "errors" "log/slog" "net/http" "github.com/go-chi/chi/v5/middleware" ) // HandlerFunc is a handler function that returns an error. // Use with Wrap() to automatically handle error responses. // // Example: // // func GetUser(w http.ResponseWriter, r *http.Request) error { // user, err := svc.Get(ctx, id) // if err != nil { // return api.NotFoundf("user %s not found", id) // } // api.WriteSuccess(w, r, user) // return nil // } type HandlerFunc func(w http.ResponseWriter, r *http.Request) error // Wrap converts a HandlerFunc to http.HandlerFunc, automatically handling // error responses. HTTPErrors are written with their status code and details; // other errors are logged and returned as 500 Internal Server Error. // // Example: // // r.Get("/users/{id}", api.Wrap(handlers.GetUser)) // // func (h *Handlers) GetUser(w http.ResponseWriter, r *http.Request) error { // id := chi.URLParam(r, "id") // user, err := h.userSvc.Get(r.Context(), id) // if err != nil { // if errors.Is(err, ErrUserNotFound) { // return api.NotFoundf("user %s not found", id) // } // return err // Will be logged and returned as 500 // } // api.WriteSuccess(w, r, user) // return nil // } func Wrap(h HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := h(w, r); err != nil { writeErrorFromErr(w, r, err) } } } // WrapWithLogger is like Wrap but accepts a logger for internal error logging. // Use this when you want to log internal errors that aren't HTTPErrors. // // Example: // // r.Get("/users/{id}", api.WrapWithLogger(handlers.GetUser, logger)) func WrapWithLogger(h HandlerFunc, logger *slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := h(w, r); err != nil { writeErrorFromErrWithLogger(w, r, err, logger) } } } // writeErrorFromErr writes an error response based on the error type. // HTTPErrors are written with their status code; other errors become 500. func writeErrorFromErr(w http.ResponseWriter, r *http.Request, err error) { writeErrorFromErrWithLogger(w, r, err, nil) } // writeErrorFromErrWithLogger writes an error response and optionally logs internal errors. func writeErrorFromErrWithLogger(w http.ResponseWriter, r *http.Request, err error, logger *slog.Logger) { var httpErr *HTTPError if errors.As(err, &httpErr) { // Write the HTTPError directly WriteError(w, r, httpErr.Status, httpErr.Code, httpErr.Message, httpErr.Details) return } // For non-HTTP errors, log and return generic 500 if logger != nil { reqID := middleware.GetReqID(r.Context()) logger.Error("internal error", "error", err, "request_id", reqID, "method", r.Method, "path", r.URL.Path, ) } WriteInternalError(w, r, "internal error") } // ----------------------------------------------------------------------------- // Middleware Helpers // ----------------------------------------------------------------------------- // MiddlewareFunc is a middleware that returns an error. // Use with WrapMiddleware() to automatically handle error responses. type MiddlewareFunc func(next http.Handler) func(w http.ResponseWriter, r *http.Request) error // WrapMiddleware converts a MiddlewareFunc to a standard http middleware. // // Example: // // func AuthMiddleware(next http.Handler) func(w http.ResponseWriter, r *http.Request) error { // return func(w http.ResponseWriter, r *http.Request) error { // token := r.Header.Get("Authorization") // if token == "" { // return api.Unauthorized("missing authorization header") // } // user, err := validateToken(token) // if err != nil { // return api.Unauthorized("invalid token") // } // ctx := context.WithValue(r.Context(), userKey, user) // next.ServeHTTP(w, r.WithContext(ctx)) // return nil // } // } // // r.Use(api.WrapMiddleware(AuthMiddleware)) func WrapMiddleware(m MiddlewareFunc) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := m(next)(w, r); err != nil { writeErrorFromErr(w, r, err) } }) } }