Middleware
Middleware wraps every request on its way to a route handler, and every response on the way back out. Use it for cross-cutting concerns (logging, authentication, CSRF, method override) that should apply across routes rather than inside one handler.
Middleware is stored as concrete function values, so the stack runs identically on the native server and the Cloudflare Worker.
The signature
A middleware is a function that receives the request and the next responder in the chain, and returns a response:
public typealias MiddlewareFunction = (Request, Responder) async throws -> ResponseIt may do three things, in any combination:
- inspect or transform the request before calling
next, - call
next(request)to continue down the chain (and eventually reach the route - inspect or transform the response
nextreturns, or skipnextentirely
handler), and
to short-circuit.
Registering middleware
Register middleware on the Application with use. The closure form is the common case:
let app = Application()
// Log every request through the platform log seam
// (console.log on Workers, stdout natively).
app.use { request, next in
let response = try await next(request)
request.context.log("\(request.method.name) \(request.path) -> \(response.status)")
return response
}To short-circuit, return a response without calling next:
app.use { request, next in
guard request.headers.first("x-api-key") == "let-me-in" else {
return .text("unauthorized", status: 401)
}
return try await next(request)
}To rewrite the request, make a mutable copy and pass it on. Request is a value type, so mutations are local until you forward them:
app.use { request, next in
var forwarded = request
forwarded.headers.set("x-request-id", makeID())
return try await next(forwarded)
}The protocol form
For reusable middleware with configuration or state, conform a concrete type to Middleware:
public protocol Middleware {
func respond(to request: Request, next: Responder) async throws -> Response
}struct RequireHTTPS: Middleware {
func respond(to request: Request, next: Responder) async throws -> Response {
if request.headers.first("x-forwarded-proto") == "http" {
return .redirect(to: "https://…", status: 308)
}
return try await next(request)
}
}
app.use(RequireHTTPS())use(_:) adapts the value to a MiddlewareFunction at registration time.
Ordering
Registration order is nesting order. The first middleware registered is the outermost: it runs first on the way in and last on the way out. Given
app.use(A)
app.use(B)
app.use(C)a request flows A → B → C → handler, and the response returns handler → C → B → A. Register broad, early-exit concerns (logging, method override, auth) before narrower ones. A typical order:
app.use(loggingMiddleware) // outermost: sees final status
app.use(methodOverride()) // rewrite POST → PUT/PATCH/DELETE before routing
app.use(csrfProtection()) // reject unsafe requests without a valid token
app.use(identityMiddleware(sessions)) // resolve request.principalError handling
Handlers and middleware are async throws. A thrown error that is not caught by a middleware propagates to the framework, which returns a 500 Internal Server Error. To translate errors into specific responses, catch them in a middleware:
app.use { request, next in
do {
return try await next(request)
} catch is NotFound {
return .status(404)
}
}Scope
Middleware registered with use runs for every request. To apply middleware to a subset of routes, use a route group (see Routing); alternatively, gate on the request inside the middleware:
app.use { request, next in
guard request.path.hasPrefix("/admin") else { return try await next(request) }
// …admin-only checks…
return try await next(request)
}Built-in middleware
PlumeKit ships several middleware factories in PlumeCore:
methodOverride(): rewrites aPOSTintoPUT/PATCH/DELETEwhen the body
carries a _method field (or an X-HTTP-Method-Override header). HTML forms can only issue GET/POST, so this lets a form drive a resourceful route. It runs before routing, so the overridden method is what matches. See Forms.
csrfProtection(secretName:): rejects unsafe requests (POST/PUT/PATCH/
DELETE) that lack a valid CSRF token, validated timing-safely against a signing secret read from the secrets binding (default "CSRF_SECRET"). JSON-body requests and bearer-token requests are exempt (they are not exposed to CSRF); it guards ambient-credential form and multipart submissions. See Forms.
identityMiddleware(_:cookieName:): resolves the authenticated identity from a
signed cookie session or an Authorization: Bearer token and sets request.principal, so downstream handlers and policies can read currentUser. See Auth.
Register the ones you need in buildApp():
app.use(methodOverride())
app.use(csrfProtection())
app.use(identityMiddleware(sessions))