Weeks 1-7 of the template upgrade plan: - pkg/api: typed HTTPError with sentinels, Wrap/WrapMiddleware, Bind, health probes, OpenAPI schema/param builders - skeleton/packages: ui (design tokens, components), layout (DashboardShell), auth (AuthProvider, ProtectedRoute), api-client - skeleton/pkg: httperror, app/handler, app/bind, app/health, auth (JWT/API key middleware) - components/app-nextjs: Next.js 14 App Router template with dashboard, server actions, auth - cookbooks/feature-development.md with test and validation scripts - Handler tests for components, project management, and woodpecker webhook - 3 rounds of code review fixes applied Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
134 lines
4.2 KiB
Go
134 lines
4.2 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|