-
Notifications
You must be signed in to change notification settings - Fork 0
Middleware
type Middleware func(http.Handler) http.HandlerThe standard Go signature, unchanged. What this module adds is the two things the bare type does not carry: a defined composition order, and a way to attach middleware to a subset of routes.
Priorities are fixed constants, not a free-for-all. Lower runs further out: the lowest priority sees the request first and the response last.
| Constant | Value | Position |
|---|---|---|
PriorityRecovery |
100 | outermost — catches panics from everything inside, including other middleware |
PriorityCORS |
200 | answers preflights and decorates responses, including error responses |
PriorityRateLimit |
300 | rejects floods before anything expensive runs |
PriorityAuth |
400 | authenticates and establishes the principal |
PriorityCSRF |
450 | verifies the request came from an allowed origin |
PriorityValidation |
500 | parses and validates the request |
PriorityHealthGate |
600 | refuses requests whose dependencies are down |
PriorityDefault |
1000 | innermost — metrics, tracing, access logging |
Equal priorities keep registration order.
Because the order is semantic, not a matter of taste, and getting it wrong produces bugs that do not look like ordering bugs:
- Rate limiting must precede authentication. Otherwise a flood of unauthenticated requests each costs a password hash or a token verification before being rejected. The limiter then protects nothing; it adds work.
- CSRF must follow authentication, because a double-submit check needs the session the authenticator established.
- Validation must follow both, or bodies get parsed for requests that were always going to be refused.
- CORS must precede everything that can reject, because a browser needs the CORS headers on the error response too — without them it reports an opaque failure instead of the actual status.
-
Recovery must be outermost, or a panic below it escapes to
net/httpand kills the connection with no response written.
Leaving that to whichever order New() happened to receive its extensions in
guarantees a subtle production bug eventually — and one that reproduces only on
the machine where the arguments were reordered.
Pick the constant that describes what your middleware is. If none fits,
PriorityDefault is right for anything that only observes.
r.Use(myMiddleware) // every router, PriorityDefaultr.UseOnRouter("metrics", basicAuth, rextension.PriorityAuth)The router need not exist yet.
type PerRouteMiddleware func(rt RouteInfo) MiddlewareThe framework calls the factory once per route, when the route table is
built and every route is known. Returning nil means not applicable: nothing
is attached to that route, so it costs nothing at request time rather than being
a no-op that still runs.
r.UsePerRoute(func(info rextension.RouteInfo) rextension.Middleware {
gated, ok := info.Route.(DependencyGatedRoute)
if !ok {
return nil // not gated — nothing attached, ever
}
deps := gated.Dependencies() // read ONCE, here
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !e.available(deps) {
rextension.WriteProblem(w, req, http.StatusServiceUnavailable,
rextension.ProblemDependencyUnavailable, "a required dependency is unavailable")
return
}
next.ServeHTTP(w, req)
})
}
}, rextension.PriorityHealthGate)Capture configuration; do not look it up. Read what the middleware needs
from rt inside the factory and close over it. The closure is built once per
route, so per-request work disappears and the configuration cannot change under
a request that is already running.
Why a factory rather than an event. Extensions used to learn about routes by subscribing to
router.route.registered, then had to find the route again per request to decide whether they applied — which meant keying an index by the raw URL path, which meant every parameterized route silently missed. The dependency gate never fired for/users/{id}; per-endpoint rate limits fell back to the global bucket. A factory inverts that: the extension declares how to decide, and the framework supplies each route at the one moment the whole table is known.
type PerRouterMiddleware func(routerName string) MiddlewareCalled once per router. This exists for middleware whose configuration depends
on the router rather than the route — the in-flight request gauge is the
motivating case, because it is labelled by router name, and that label has to be
resolved once at composition time. With only Use available the alternatives
were both wrong: hardcode one router's name and mislabel every other router's
requests, or look the name up per request and pay a map lookup and a lock for a
value that never changes.
type RouteInfo struct {
Route route.Route // the registered route
Router string // the name of the router it is registered on
BaseURL string // that router's normalised base path prefix
}The router name is in the payload because an extension may need it and cannot otherwise get it. The rate limiter's precedence chain is endpoint → router → global, so which limit applies to a route depends on which router serves it; the OpenAPI generator includes or excludes a route based on the same thing, so an internal-only listener's routes stay out of the public document.
BaseURL is normalised: no trailing slash, and "" for the root. A subscriber
that needs the URL a route is actually served at combines Path with this — the
route knows its own pattern but not the prefix its router mounts it under.
Chains are composed once, when the route table is built — not per request. Rebuilding a chain per request cost one closure allocation per middleware per request for a result that never varied.
Within one route's chain, entries are sorted by priority ascending, ties broken by registration order, then wrapped from the inside out.
Nothing here is special, but two conventions keep the ecosystem coherent:
Answer with a Problem document. Every extension in the ecosystem does, so a client parses one error format rather than four.
rextension.WriteProblem(w, req, http.StatusTooManyRequests,
rextension.ProblemRateLimitExceeded, "rate limit exceeded")Pass information down the context, not through package state. The matched route is already there:
rt, ok := rxroute.GetMatchedRoute(req)rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem