Middleware and Security
Middleware and Security at the Askr server boundary, with validated input and explicit failure states.
Example
Install middleware in composition order: request identity and telemetry first, security and domain middleware next, terminal errors last.
const router = createRouter()
.use(requestIds(), telemetry(), securityHeaders())
.use(authMiddleware(auth))
.get('/projects', listProjects);Middleware order
A Middleware is (ctx, next) => Response | Promise<Response> — call next() to continue down the chain, or return your own Response to short-circuit it. Middleware registered with router.use(...) runs in the order it's added, and because each layer wraps the next one, code written before next() runs on the way in and code written after it runs on the way out, once the inner handler has produced a response. Route-level middleware from ApiRouteOptions.middleware runs closer to the handler than anything registered on the router itself.
Security headers
securityHeaders(options) sets response headers like Content-Security-Policy, Referrer-Policy, and X-Frame-Options, each overridable through the contentSecurityPolicy, referrerPolicy, and frameOptions options rather than hardcoded. cors(options) handles the CORS handshake separately — origin can be a fixed string or a function of the request origin and context, and methods, allowedHeaders, exposedHeaders, credentials, and maxAgeSeconds all map to the corresponding response headers. enforceHttps() is a third, narrower piece of the same family that redirects plain HTTP traffic to HTTPS.
Cookies and redirects
ctx.setCookie(response, name, value, options) and ctx.clearCookie(response, name, options) both take a Response and return a new one with the Set-Cookie header attached — CookieOptions covers domain, path, expires, maxAge, httpOnly, secure, and sameSite ("strict" | "lax" | "none"). Per the README, these only modify the response; they don't establish a session or check a password, so pairing setCookie() with an actual auth check is on the caller. ctx.redirect(location, status?) defaults to 302 but accepts 301, 303, 307, or 308, and ctx.challenge(options) is the companion for auth failures — it sets WWW-Authenticate with a configurable scheme and realm and returns 401 by default, or 407 on request.
Error handling
ServerAppOptions.onError(error, context) is where an unhandled exception from a handler or middleware ends up — return a Response from it and that's what the client receives, instead of the framework's default. It's not the only place errors are turned into responses, though: a handful of framework-recognized failures (an oversized body, a malformed path parameter, a failed ctx.bind()) are converted to a fixed problem+json response before onError runs at all, so treat it as the catch-all for your own thrown errors rather than the single funnel for every failure. Because ctx.problem() and its status-helper wrappers are available on the context passed into onError, a typical implementation just logs the error and returns ctx.internalServerError() or a custom problem+json body. The canonical application path registers routes through a Router; defineRoutes() and raw ApiRoute[] values remain lower-level compatibility inputs.