Middleware reference
Every middleware Breeze ships, what each does, and the order to install them in.
import middleware "github.com/nelthaarion/breeze/v2/middlewares"
router.Use(middleware.RecoveryMiddleware()) // outermost
router.Use(middleware.LoggingMiddleware())
router.Use(middleware.NewRateLimiter(middleware.RateLimiterOptions{
Requests: 100,
Per: time.Minute,
}))
router.Use(middleware.CORSMiddleware(middleware.CORSOptions{AllowOrigins: "*"}))
router.Use(middleware.DefaultSecurityMiddleware())
router.Use(middleware.CompressionMiddleware()) // innermost The package is middlewares on disk and middleware in Go — every import
site aliases it, as above. Renaming either breaks every existing import
line, so it stays that way deliberately.
The contract
A middleware is a breeze.HandlerFunc. It runs before the handler, calls ctx.Next() to continue, and returns an error — the same signature as a
handler:
func requireAPIKey(ctx *breeze.Context) error {
if ctx.GetHeader("x-api-key") != wantKey {
ctx.Status(401)
return ctx.JSON(map[string]string{"error": "api key required"})
}
return ctx.Next()
} Anything read from ctx.Req must be read before ctx.Next() — request
strings point into a pooled buffer the next request on that connection
reuses. Response headers must be set before Next() too, since after it
returns the handler has already written the response.
Installation order
Order is not a preference — each row is a consequence of what the middleware does to the request or response:
| # | Middleware | Why here |
|---|---|---|
| 1 | RecoveryMiddleware | Outermost, or it cannot catch a panic raised by anything installed after it. |
| 2 | LoggingMiddleware | Outside the rate limiter, so a rejected request still appears in the log. |
| 3 | NewRateLimiter | Before auth and any handler work, so a flood costs a map lookup, not a token verification. |
| 4 | LocaleMiddleware | Before anything that renders text. |
| 5 | CORSMiddleware | Must answer the OPTIONS preflight before auth rejects it — a preflight carries no Authorization header. |
| 6 | SecurityMiddleware | After CORS: it only adds response headers, and putting it first lets CORS overwrite them. |
| 7 | JWTAuthMiddleware | After CORS, before the handler. |
| 8 | ETagMiddleware | Inside auth, so a 304 is only ever served to a caller who was allowed to see the body. |
| 9 | CompressionMiddleware | Innermost — it rewrites the finished body, so everything that inspects or sets it must already have run. |
ScalarMiddleware is a route, not a chain member — install it once, not
with Use. See OpenAPI / Scalar.
The middlewares
RecoveryMiddleware()
Recovers a panic, logs it with the stack, and returns a 500. Feeds the recovery diagnostic probe with the panic count and the most recent panic's
time and value — panic facts are counted ungated, unlike every other
counter here, because a panic is rare enough that the atomic increment is
never on a hot path.
LoggingMiddleware()
One line per request: method, path, status, duration. Reads everything it
needs before Next.
NewRateLimiter(RateLimiterOptions)
Fixed-window counting, keyed by client IP with the port stripped so a reconnect shares its counter.
| Field | Type | Default | Meaning |
|---|---|---|---|
Requests | int | 0 (blocks everything) | requests allowed per window |
Per | time.Duration | 0 | the window |
Message | string | "Rate limit exceeded: max N requests per D" | 429 body |
Set both Requests and Per — there is no default, deliberately: a rate
limiter that silently picked a limit would be worse than one that obviously
blocks. The lock is held only for the map lookup and counter update, never
across ctx.Next(); the client map is swept once a minute by a background
goroutine.
CORSMiddleware(CORSOptions)
All fields are strings, matching the header values they become:
| Field | Example |
|---|---|
AllowOrigins | "*" or "https://example.com" |
AllowMethods | "GET,POST,PUT,DELETE" |
AllowHeaders | "Content-Type,Authorization" |
ExposeHeaders | "X-Request-Id" |
AllowCredentials | "true" |
MaxAge | "86400" (seconds) |
AllowOrigins: "*" with AllowCredentials: "true" is rejected by browsers,
not by this middleware — name the origin when you need credentials.
SecurityMiddleware(SecurityOptions) / DefaultSecurityMiddleware()
Twelve response headers: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS, Permissions-Policy, X-XSS-Protection, Expect-CT, the three Cross-Origin-*-Policy headers, and Cache-Control. DefaultSecurityMiddleware() is the strict set; override one field at a
time:
router.Use(middleware.SecurityMiddleware(
middleware.WithContentSecurityPolicy("default-src 'self'; img-src *"),
)) CompressionMiddleware()
Negotiates Accept-Encoding and compresses with brotli, gzip, or deflate —
in that preference order, since brotli wins on ratio for text and every
browser sending br supports it. Encoders are pooled per algorithm; a fresh
brotli encoder costs 4–8 KB, and that allocation does not belong on the hot
path.
NewETagCache() + ETagMiddleware()
MD5 of the response body as a strong ETag, with a 304 when If-None-Match matches. The cache key includes the query string, so ?page=1 and ?page=2 get distinct ETags. MD5 because it is already in the standard
library and the body is already in memory — this is a change detector, not
a security boundary.
The store is unbounded. It is written on every response and read only on
an If-None-Match, so it exists for observability rather than for serving
from cache. Do not install this on an endpoint with unbounded distinct URLs
without adding eviction.
JWTAuthMiddleware(JWTOptions)
| Field | Type | Purpose |
|---|---|---|
AccessSecret | string | HMAC key — required |
RefreshSecret | string | key for refresh tokens |
SigningMethod | jwt.SigningMethod | pinned; e.g. jwt.SigningMethodHS256 |
TokenLookup | func(*Context) (string, string, error) | defaults to Authorization: Bearer |
OnUnauthorized | func(*Context, error) | defaults to a 401 problem+json |
UserContextKey | string | where claims land in ctx |
RequiredRoles | []string | role gate |
ClaimsValidator | func(jwt.MapClaims) bool | extra claim checks |
EnableRefreshToken | bool | accept and rotate refresh tokens |
It refuses to construct without AccessSecret. An empty HMAC key
verifies any token an attacker signs with an empty key — a missing secret is
not a degraded configuration, it is an open door, so failing at startup is
the only safe response. SigningMethod is pinned rather than read from the
token's own header, which closes the alg: none and RS256→HS256 confusion
attacks. GenerateJWT and GenerateRefreshToken are exported for your
login handler.
LocaleMiddleware(*breeze.I18n)
Resolves the request locale from ?lang=, then the breeze_locale cookie,
then Accept-Language, and attaches it to the context for the template
engine. See Templates, SPA & i18n.
ScalarMiddleware(router, ScalarOptions)
Not a chain middleware — it registers the OpenAPI JSON and the Scalar UI as routes. See OpenAPI / Scalar.
Diagnostics
Nine of these register a diag probe, so GET /dashboard/api/diagnostics reports whether each is installed and what it has done — "not installed"
and "installed and quiet" both read as a zero count, and the probe is
careful to tell them apart. Counted middlewares (compression, ETag, rate
limit) count only once diag.EnableCounters() has been called. See Diagnostics.