Files
zip/middleware/middleware.go
zooqueenandHanzo Dev 6cc782199b an app reports because it serves: telemetry is the framework's
Every program built on zip now emits one log line per request through its own
luxfi/log logger — method, path, status, duration, trace and span, plus the
caller when the environment parked one — and RED metrics through luxfi/metric,
with W3C trace context joined when a request carries it, minted when it does
not, and propagated on Call with no handler code. /metrics renders the one
registry, and export speaks the shipped ZAP exporter, which is what o11y
ingests. Config.Telemetry{Addr, Off} is the only switch; the default is on.

The boundary is installed outermost by materialise, so requests that match
nothing — 404s, preflights — are still counted; per-route composition cannot
see them. Two defects the tests forced out: the status is derived from the
handler's error, because at the boundary the response still reads 200 (fiber
writes the real status after unwinding, so every refusal would have recorded
as success); and the route label reads the not-found sentinel, because an
unmatched request leaves the middleware's own registration in place and would
otherwise be indistinguishable from a real root leaf.

middleware.Logger and middleware.Telemetry are deleted, not deprecated:
telemetry an app can lack is the defect this closes.

luxfi/metric moves to v1.9.1 — v1.9.0 is the published build whose registry
constructors sit behind a build tag nothing sets, so counters accepted Inc()
into a registry nothing gathers and /metrics served empty.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 14:36:48 -07:00

144 lines
4.6 KiB
Go

// Package middleware ships zip's canonical generic middleware stack.
// Use these via app.Use(middleware.Recover(), middleware.RequestID(), ...).
//
// Every middleware here is a zip.Handler (NOT a raw fiber.Handler) so
// the user-facing handler signature stays uniform.
//
// Auth-specific middleware (JWT validation, identity-header stripping)
// lives in github.com/hanzoai/gateway/middleware — see the package
// README for the rationale.
package middleware
import (
"crypto/rand"
"encoding/hex"
"runtime/debug"
"strings"
"time"
"github.com/zap-proto/zip"
)
// Recover catches handler panics and turns them into a 500 JSON response.
// Always include this first in the chain.
func Recover() zip.Handler {
return func(c *zip.Ctx) error {
defer func() {
if r := recover(); r != nil {
c.Log().Error("zip panic recovered",
"err", r,
"path", c.Path(),
"method", c.Method(),
"stack", string(debug.Stack()),
)
_ = c.JSON(500, &zip.HTTPError{
Status: 500,
Msg: "internal server error",
})
}
}()
return c.Continue()
}
}
// RequestID injects an X-Request-Id header (incoming if present; else
// 16-byte hex). Available via c.RequestID().
func RequestID() zip.Handler {
return func(c *zip.Ctx) error {
rid := c.Header("X-Request-Id")
if rid == "" {
var b [16]byte
_, _ = rand.Read(b[:])
rid = hex.EncodeToString(b[:])
c.Fiber().Request().Header.Set("X-Request-Id", rid)
}
c.SetHeader("X-Request-Id", rid)
return c.Continue()
}
}
// The request log line and the o11y sink used to live here, as Logger(base)
// and Telemetry(sink). Both are gone, and neither was replaced by anything a
// caller writes: zip logs every request, measures it, and carries trace
// context because the app is serving. See zip.Telemetry.
//
// They were middleware, and middleware is opt-in. That is the right shape for a
// policy — a rate limit, a CORS rule — and the wrong shape for the report a
// program makes about itself, because opt-in telemetry is telemetry some binary
// does not have, and which one is always discovered during the incident where
// it was needed. Moving it into the framework did not add a feature; it removed
// the possibility of an app that lacks it.
// Timeout sets a per-request deadline via context.WithTimeout. Handlers
// that respect ctx will be cancelled when it expires.
func Timeout(d time.Duration) zip.Handler {
return func(c *zip.Ctx) error {
// Fiber v3's fasthttp-backed ctx doesn't propagate stdlib
// context cancellation through the request lifetime (see fiber
// docs on Done/Err). The deadline is best-effort here — useful
// for downstream code that pulls c.Context() and threads it
// into its own clients (DB, HTTP, etc.).
_ = d
return c.Continue()
}
}
// MaxBody refuses requests larger than n bytes with 413.
func MaxBody(n int) zip.Handler {
return func(c *zip.Ctx) error {
if len(c.Body()) > n {
return zip.Errorf(413, "request body too large")
}
return c.Continue()
}
}
// CORSConfig configures the CORS middleware.
type CORSConfig struct {
AllowOrigins []string // "*" or explicit list. Default: ["*"]
AllowMethods []string // Default: GET,POST,PUT,DELETE,PATCH,OPTIONS
AllowHeaders []string // Default: Content-Type,Authorization,X-Request-Id
ExposeHeaders []string
AllowCreds bool
MaxAge int // seconds
}
// CORS returns the CORS middleware.
func CORS(cfg CORSConfig) zip.Handler {
if len(cfg.AllowOrigins) == 0 {
cfg.AllowOrigins = []string{"*"}
}
if len(cfg.AllowMethods) == 0 {
cfg.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}
}
if len(cfg.AllowHeaders) == 0 {
cfg.AllowHeaders = []string{"Content-Type", "Authorization", "X-Request-Id"}
}
origins := strings.Join(cfg.AllowOrigins, ",")
methods := strings.Join(cfg.AllowMethods, ",")
headers := strings.Join(cfg.AllowHeaders, ",")
expose := strings.Join(cfg.ExposeHeaders, ",")
return func(c *zip.Ctx) error {
c.SetHeader("Access-Control-Allow-Origin", origins)
c.SetHeader("Access-Control-Allow-Methods", methods)
c.SetHeader("Access-Control-Allow-Headers", headers)
if expose != "" {
c.SetHeader("Access-Control-Expose-Headers", expose)
}
if cfg.AllowCreds {
c.SetHeader("Access-Control-Allow-Credentials", "true")
}
if cfg.MaxAge > 0 {
c.SetHeader("Access-Control-Max-Age", time.Duration(cfg.MaxAge).String())
}
if c.Method() == "OPTIONS" {
return c.NoContent(204)
}
return c.Continue()
}
}
// Auth-specific middleware (JWT validation, identity-header stripping)
// has moved to github.com/hanzoai/gateway/middleware. See that package's
// README and zip/middleware/README.md for the rationale.