🌬️ Breeze docs

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:

#MiddlewareWhy here
1RecoveryMiddlewareOutermost, or it cannot catch a panic raised by anything installed after it.
2LoggingMiddlewareOutside the rate limiter, so a rejected request still appears in the log.
3NewRateLimiterBefore auth and any handler work, so a flood costs a map lookup, not a token verification.
4LocaleMiddlewareBefore anything that renders text.
5CORSMiddlewareMust answer the OPTIONS preflight before auth rejects it — a preflight carries no Authorization header.
6SecurityMiddlewareAfter CORS: it only adds response headers, and putting it first lets CORS overwrite them.
7JWTAuthMiddlewareAfter CORS, before the handler.
8ETagMiddlewareInside auth, so a 304 is only ever served to a caller who was allowed to see the body.
9CompressionMiddlewareInnermost — 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.

FieldTypeDefaultMeaning
Requestsint0 (blocks everything)requests allowed per window
Pertime.Duration0the window
Messagestring"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:

FieldExample
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)

FieldTypePurpose
AccessSecretstringHMAC key — required
RefreshSecretstringkey for refresh tokens
SigningMethodjwt.SigningMethodpinned; e.g. jwt.SigningMethodHS256
TokenLookupfunc(*Context) (string, string, error)defaults to Authorization: Bearer
OnUnauthorizedfunc(*Context, error)defaults to a 401 problem+json
UserContextKeystringwhere claims land in ctx
RequiredRoles[]stringrole gate
ClaimsValidatorfunc(jwt.MapClaims) boolextra claim checks
EnableRefreshTokenboolaccept 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.

Generated documentation for the Breeze framework · built with SvelteKit, fully static.