Filters and Errors
A request filter runs before routing, a route filter runs around a matched handler, and a response filter runs on every response.
An exception handler turns an exception a handler threw into a response.
An error handler fills in the body of a response that ended on a status with nothing to show.
The diagram shows where each one runs, for one request through AppServlet.
Static files answer only when no route matched, and only for GET and HEAD.
The response-wide concerns from CORS, Compression, and Security run after every answer, the static file and the error response included, which is why they are not after-filters.
Filters
beforeRequest runs before routing for every matching request, including static files, 404s, 405s, HEAD, and automatic OPTIONS.
beforeRoute runs only for a matched route and exposes its path variables, and the route itself through req.route().
afterRoute runs only when that handler returns normally.
All three take an optional path pattern and cover their whole scope when given none.
A route group’s beforeRequest resolves its prefix in the same way as its route filters.
Path variables are unavailable in request filters, even when the filter pattern contains a variable.
Use beforeRequest when authorization must also cover static files and unmatched paths:
app.beforeRequest("/admin/*", req -> req.sessionAttr("user") == null
? WebResponse.redirect("/login")
: null);
A request filter’s early response or exception uses the normal template, error, response-filter, and decoration pipeline. Request filters execute in registration order before all route filters, regardless of the order the two kinds were registered. A request filter can reject an OPTIONS request before CORS preflight handling, so its authorization policy must allow preflights when required.
For a guard that needs a matched route’s path variables, use beforeRoute.
A trailing * covers the prefix and everything under it, so /admin/* guards /admin as well as /admin/users, which is what a guard almost always means:
app.beforeRoute("/admin/*", req -> req.sessionAttr("user") == null
? WebResponse.redirect("/login") // answers here, so the route handler never runs
: null); // carry on
A before-filter that returns a response ends the request there.
Returning null continues to the next filter and then to routing or the matched handler.
Throw new HttpException(HttpStatus.UNAUTHORIZED, "…") to reject without a body of your own, and let error(HttpStatus.UNAUTHORIZED, …) render it.
An after-filter takes the response the route returned and hands back a replacement, or that same response to leave it unchanged:
app.afterRoute("/api/*", (req, res) -> res.header("Cache-Control", "no-store"));
An after-filter runs only when a route handler returned normally.
A before-filter’s early answer, an exception handler’s answer, a router 404 or 405, the automatic OPTIONS answer, and a static file never reach it.
beforeRoute("/admin/*", ...) does not run for a static file served under /admin.
Use beforeRequest("/admin/*", ...) to guard those files as well.
Response filters
responseFilter(filter) runs on every response the framework answers, including the ones an after-filter never sees.
A header that every response has to carry belongs here:
app.responseFilter((req, res) -> res.header("X-Request-Id", requestId()));
The filter takes the request and the response about to be sent, and returns a replacement or the response passed in to leave it unchanged.
Returning null from afterRoute or responseFilter is a programming error and follows exception handling.
Several filters run in registration order, each on what the one before it answered.
The request carries the path variables of the route that matched, when one did.
A response filter runs once the answer has been worked out and before it is decorated:
-
Request filters run before routing and may answer immediately.
-
Route filters and the matched handler answer, or static files and routing produce the answer.
-
An exception handler answers for anything that threw, the template is rendered, and an error handler fills an empty error body.
-
The response filters run, and a template body one of them returns is rendered.
-
Vary: Accept, CORS, the security headers, and compression are applied, in that order. -
The response is written and logged.
A response filter therefore sees the error body and the rendered page, and cannot undo CORS, a security header, or compression, since those are applied to what it returns.
Its answer is final: an empty 404 it returns does not go back to error(HttpStatus.NOT_FOUND, …) for a body.
A filter that throws is answered the way a handler that throws is.
The exception reaches the exception(Type, handler) handlers, then error(status, …), and the answer to it is decorated and written without running the response filters again, so a filter that always fails cannot fail in a loop.
A response filter shapes a response and does not authorize a request.
The handler has already run by the time it is called, so authorization belongs in beforeRequest or beforeRoute, depending on its scope.
What a WebResponse.raw(…) writer writes to the servlet response, and a failure after the response is committed, happen after the filter and are out of its reach.
Exception handlers
exception(Type, handler) answers a request whose handler threw an exception of that type or a subtype:
app.exception(IllegalArgumentException.class,
(req, e) -> WebResponse.text(e.getMessage()).status(HttpStatus.NOT_FOUND));
app.exception(Json.JsonException.class,
(req, e) -> WebResponse.text(e.getMessage()).status(HttpStatus.BAD_REQUEST));
The handler for the most specific type the exception is an instance of runs, whatever order the handlers were registered in.
Json.JsonException is an IllegalArgumentException, and a body that failed to parse reaches the second handler above rather than the first.
Registering a type twice replaces the first handler.
An HttpException is a status the handler chose, not a failure.
It answers with its status and message and goes to error(status, …) for its body, and a handler registered for a broader type such as RuntimeException never sees it.
A handler registered for HttpException itself, or for a subtype of it, does.
Any other uncovered exception answers 500 and is logged.
Whichever answered it, the exception is reported to the request logger as completion.exception(), so recording failures does not take a catch-all handler.
A template that fails to render throws inside the handler’s own dispatch, so it reaches these handlers too.
Error handlers
error(status, handler) fills in the body for any response that ended on that status with no body, whether from the router, from an HttpException, or from a handler that returned WebResponse.empty(HttpStatus.FORBIDDEN).
error(HttpStatus.NOT_FOUND, handler) therefore styles the router’s own 404 and every handler-made one alike, in one registration.
A response that already carries a body is left alone.
What the error handler returns keeps the headers the framework had already worked out, such as the Allow of a 405.
It answers with the registered status unless it sets one of its own.
Inside an error handler, req.errorMessage() is the plain-text message the framework would have used.
The registrations are readable back.
app.guards() reports the filters, the response filters, and the error handlers the way app.routes() reports the routes, in Route Introspection.