Middleware
Middleware in λ Cosmos wraps handlers to add cross-cutting behavior.
type Handler func(w http.ResponseWriter, r *http.Request) error
type Middleware = func(Handler) HandlerApply middleware with Use:
app := framework.New()app.Use(middleware.Recover())app.Use(middleware.Logger(logger))Middleware runs in registration order — the first Use call wraps outermost.
Recover
Section titled “Recover”Catches panics and converts them into errors that flow through the normal error handling chain:
import "github.com/studiolambda/cosmos/framework/middleware"
app.Use(middleware.Recover())The default recovery handler converts panic values to errors based on their type:
| Panic type | Conversion |
|---|---|
error | Returned as-is |
string | Wrapped in errors.New() |
fmt.Stringer | Uses String() method |
io.Reader | Reads all bytes |
encoding.TextMarshaler | Uses MarshalText() |
| Other | Formatted with fmt.Sprintf and joined with ErrRecoverUnexpected |
All recovered panic values are wrapped with ErrRecoverUnexpected via errors.Join to prevent internal details from leaking to HTTP clients. The original value remains accessible via errors.Unwrap for logging. For io.Reader panic values, the content is capped at 1 MB.
For custom panic handling, use RecoverWith:
app.Use(middleware.RecoverWith(func(value any) error { // custom panic-to-error conversion sentry.CaptureException(fmt.Errorf("panic: %v", value)) return fmt.Errorf("internal error")}))Logger
Section titled “Logger”Logs failed requests using structured logging (log/slog):
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))app.Use(middleware.Logger(logger))The logger fires after the response is complete and logs when:
- The handler returned an error, or
- The response status code is 5xx.
Logged fields: method, url, status, err.
Passing nil creates a discard logger (no panics, no output).
Protects against Cross-Site Request Forgery using Go’s built-in http.CrossOriginProtection:
app.Use(middleware.CSRF("https://example.com", "https://admin.example.com"))Pass trusted origins that are allowed to make state-changing requests. Requests from untrusted origins receive a 403 Forbidden Problem Details response.
For full control over the CSRF configuration:
csrf := http.NewCrossOriginProtection()csrf.AddTrustedOrigin("https://example.com")
app.Use(middleware.CSRFWith(csrf, problem.Problem{ Title: "CSRF Blocked", Detail: "Your custom error message", Status: http.StatusForbidden,}))Provide
Section titled “Provide”Injects values into the request context, making them available to downstream handlers:
app.Use(middleware.Provide(myKey, myValue))For dynamic values computed per request, use ProvideWith:
app.Use(middleware.ProvideWith(func(w http.ResponseWriter, r *http.Request) (context.Context, error) { user, err := authenticate(r) if err != nil { return nil, err } return context.WithValue(r.Context(), userKey, user), nil}))If the ProvideFunc returns an error, the request short-circuits and the error flows through normal error handling.
HTTP Adapter
Section titled “HTTP Adapter”Integrate standard Go func(http.Handler) http.Handler middleware with Cosmos:
import "github.com/rs/cors"
corsMiddleware := cors.New(cors.Options{ AllowedOrigins: []string{"https://example.com"},}).Handler
app.Use(middleware.HTTP(corsMiddleware))This adapter bridges the gap between Go’s standard middleware pattern and Cosmos’s error-returning handlers. It:
- Converts the Cosmos handler chain into a standard
http.Handler. - Applies the HTTP middleware.
- Captures any errors from downstream Cosmos handlers.
- Returns captured errors through the Cosmos middleware chain.
This means you can use any third-party Go HTTP middleware (CORS, rate limiting, etc.) without losing Cosmos error handling.
Configurable Cross-Origin Resource Sharing headers for APIs accessed from browsers on different domains:
app.Use(middleware.CORS(middleware.CORSOptions{ AllowedOrigins: []string{"https://example.com", "https://app.example.com"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowedHeaders: []string{"Authorization", "Content-Type"}, ExposedHeaders: []string{"X-Request-ID"}, AllowCredentials: true, MaxAge: 86400,}))Use middleware.DefaultCORSOptions as a starting point and customize as needed.
Secure Headers
Section titled “Secure Headers”Adds security-related HTTP headers to all responses:
app.Use(middleware.SecureHeaders())Default headers set:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originX-XSS-Protection: 0(modern browsers use CSP instead)Strict-Transport-Security: max-age=63072000; includeSubDomains
Customize specific headers:
app.Use(middleware.SecureHeadersWith(middleware.SecureHeadersOptions{ FrameOptions: "SAMEORIGIN", ContentSecurityPolicy: "default-src 'self'", PermissionsPolicy: "camera=(), microphone=()",}))Rate Limit
Section titled “Rate Limit”Token bucket rate limiting to prevent abuse:
app.Use(middleware.RateLimit())Defaults: 15 requests per second per IP, burst of 30. Returns a 429 Too Many Requests Problem Details response when exceeded. Idle entries are automatically evicted after 5 minutes to prevent unbounded memory growth.
Customize the limits:
app.Use(middleware.RateLimitWith(middleware.RateLimitOptions{ RequestsPerSecond: 5, Burst: 10, KeyFunc: func(r *http.Request) string { return r.Header.Get("X-API-Key") // rate limit by API key instead of IP }, CleanupInterval: 2 * time.Minute, // how often to sweep idle entries MaxIdleTime: 10 * time.Minute, // evict after 10 min of inactivity}))Writing Custom Middleware
Section titled “Writing Custom Middleware”A custom middleware is just a function that takes and returns a framework.Handler:
func Timing() framework.Middleware { return func(next framework.Handler) framework.Handler { return func(w http.ResponseWriter, r *http.Request) error { start := time.Now() err := next(w, r) slog.Info("request handled", "duration", time.Since(start)) return err } }}
app.Use(Timing())Middleware can short-circuit by returning an error without calling next:
func RequireAuth(token string) framework.Middleware { return func(next framework.Handler) framework.Handler { return func(w http.ResponseWriter, r *http.Request) error { if request.Header(r, "Authorization") != "Bearer "+token { return problem.Problem{ Title: "Unauthorized", Status: http.StatusUnauthorized, } } return next(w, r) } }}