-
Notifications
You must be signed in to change notification settings - Fork 0
Middleware
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
app.Use(LoggingMiddleware)The standard Go signature, unchanged. What rex adds is a defined composition order and a way to attach middleware to a subset of routes.
Lower runs further out. The lowest priority sees the request first and the response last.
| Constant | Value | |
|---|---|---|
rextension.PriorityRecovery |
100 | outermost — catches panics from everything inside |
rextension.PriorityCORS |
200 | preflights, and headers on error responses too |
rextension.PriorityRateLimit |
300 | rejects floods before anything expensive |
rextension.PriorityAuth |
400 | authenticates, establishes the principal |
rextension.PriorityCSRF |
450 | needs the session auth established |
rextension.PriorityValidation |
500 | parses the body only for requests that survived |
rextension.PriorityHealthGate |
600 | refuses when this route's dependencies are down |
rextension.PriorityDefault |
1000 | innermost — metrics, tracing, access logging |
Equal priorities keep registration order.
The scale is fixed because the order is semantic. Rate limiting after
authentication means every flooded request costs a password hash before being
rejected — the limiter protects nothing and adds work. CSRF before
authentication has no session to check. Recovery anywhere but outermost lets a
panic escape to net/http and kill the connection with no response written.
Leaving that to whichever order New() received its extensions in guarantees a
subtle production bug eventually, and one that reproduces only on the machine
where the arguments were reordered.
app.Use(mw) registers at PriorityDefault. For anything else, name the
priority.
// Every router, PriorityDefault.
app.Use(mw)
// One named router, explicit priority. The router need not exist yet.
app.UseOnRouter("admin", basicAuth, rextension.PriorityAuth)
// A factory consulted once per route, on every router.
app.UsePerRoute(factory, rextension.PriorityHealthGate)
// The same, limited to one router.
app.UsePerRouteOn("api", factory, rextension.PriorityValidation)
// A factory consulted once per router.
app.UsePerRouter(factory, rextension.PriorityDefault)app.UsePerRoute(func(info rextension.RouteInfo) rextension.Middleware {
gated, ok := info.Route.(DependencyGatedRoute)
if !ok {
return nil // not applicable — nothing is attached to this route
}
deps := gated.Dependencies() // read ONCE, at build time
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !available(deps) {
rextension.WriteProblem(w, r, http.StatusServiceUnavailable,
rextension.ProblemDependencyUnavailable, "a dependency is unavailable")
return
}
next.ServeHTTP(w, r)
})
}
}, rextension.PriorityHealthGate)The factory is called once per route, when the table is built and every
route is known. Returning nil means not applicable: nothing is attached, so
the route pays nothing at request time rather than paying for a no-op call.
Capture configuration in the factory; never look it up per request. The closure is built once, so per-request work disappears and configuration cannot change under a request already running.
RouteInfo carries the route, the router name, and that router's normalised
BaseURL — because some middleware needs to know which listener serves a route
(the rate limiter's precedence chain is endpoint → router → global).
app.UsePerRouter(func(routerName string) rextension.Middleware {
gauge := inFlight.WithLabel(routerName) // resolved once, here
return func(next http.Handler) http.Handler { … }
}, rextension.PriorityDefault)For middleware whose configuration depends on the router rather than the
route. The alternative with only Use available is to hardcode one router's
name and mislabel every other router's traffic, or to look the name up per
request and pay a map lookup and a lock for a value that never changes.
Chains are composed when the route table is built, not per request. Within one route's chain, entries are sorted by priority ascending, ties broken by registration order, then wrapped from the inside out.
Rebuilding the chain per request cost one closure allocation per middleware per request, for a result that never varied.
Answer with a problem document. Every extension does, so a client parses one error format.
rextension.WriteProblem(w, r, http.StatusTooManyRequests,
rextension.ProblemRateLimitExceeded, "rate limit exceeded")Reach the matched route from the context, not by re-parsing the URL:
rt, ok := rxroute.GetMatchedRoute(r)Do not write after calling next unless you have wrapped the
ResponseWriter — the handler has probably already committed the status.
Recovery belongs at PriorityRecovery. rex does not install one for you;
if you want panics turned into a 500 rather than a dropped connection, add one:
app.UseOnRouter(rex.DefaultRouterName, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
log.Printf("panic: %v\n%s", v, debug.Stack())
rextension.WriteProblem(w, r, 500,
rextension.ProblemInternal, "the request could not be completed")
}
}()
next.ServeHTTP(w, r)
})
}, rextension.PriorityRecovery)rex — Restful Extended eXperience · MIT · © 2026 Kryovyx · pre-1.0 (alpha), interfaces may change
Getting started
Core
Operating
Ecosystem