Filters
These are a set of utility filters that come with Invirt and which you can opt for in your application.
CatchAll
Logs all exceptions and maps exception types to HTTP response statuses.
When an exception is caught that isn't mapped, a Status.INTERNAL_SERVER_ERROR is returned.
val handler = CatchAll(
ValidationException::class to Status.BAD_REQUEST,
AuthorisationException::class to Status.NOT_FOUND
).then(routes(/* ... */))
Each caught exception is logged through kotlin-logging with the constant
message Request failed with an exception, so that events can be grouped on it. The status the filter answers with
and the exception message are in the payload, under status and errorMessage, and the exception is the cause of
the event, so its stack trace is logged too.
The level follows the status the filter answers with. A status of 500 or above, which includes the
Status.INTERNAL_SERVER_ERROR returned for an unmapped exception, is logged at ERROR. Anything below 500 is logged
at WARN: an exception you map to a 4xx is an outcome your application chose, so it stays out of the ERROR events
that alerting typically watches.
ErrorPages
Automatically renders a Pebble template for specified HTTP error statuses. The HTTP status of the underlying response is preserved in the final response.
// Renders the template "error/404.peb"
val httpHandler = ErrorPages(Status.NOT_FOUND to "error/404")
.then(routes(/* ... */))
Rendering the error page can itself fail - a context variable that reads a downed database, a renamed
template, a macro that throws - and it fails while the application is already handling an error.
fallbackBody is what the client gets in that case, with the status the handler returned, so a raw stack
trace never reaches a browser:
val httpHandler = ErrorPages(
Status.NOT_FOUND to "error/404",
Status.INTERNAL_SERVER_ERROR to "error/500",
fallbackBody = javaClass.getResource("/static-500.html")!!.readText()
).then(routes(/* ... */))
The fallback is served as text/html; charset=utf-8, so it has to be self-contained markup - load it at
startup rather than rendering it, since a page built from a template can fail the same way. With no
fallbackBody the response is the status and an empty body. Only an Exception is caught; an Error
escapes deliberately.
StatusOverride
Overrides HTTP response status codes. The example below combines StatusOverride and ErrorPages to render
a "page not found" response when a user attempts to access a secured resource.
// securityFilter returns a Status.FORBIDDEN when a user
// tries to access a secure resource/page
ErrorPages(Status.NOT_FOUND to "error/404")
.then(StatusOverride(Status.FORBIDDEN to Status.NOT_FOUND))
.then(securityFilter)
.then(routes)
HeadAsGet
Answers a HEAD request by running the handler as GET and discarding the response body. http4k's routing
matches the request method by strict equality, so a route bound only to GET otherwise answers HEAD with a
Status.METHOD_NOT_ALLOWED - which trips up uptime monitors and link-preview services, since they check that a
page is alive with a HEAD request rather than a GET.
HeadAsGet wraps a handler instead of being a Filter: Filter.then(RoutingHttpHandler) applies a filter to
each route only after the method has been matched, which is too late to rewrite HEAD. Compose filters that must
see the original method (an access log) outside it, and filters whose bodies must be dropped for HEAD (error
pages) inside it:
HttpAccessLog()
.then(
HeadAsGet(
ErrorPages(Status.NOT_FOUND to "error/404")
.then(routes(/* ... */))
)
)
HttpAccessLog
Logs HTTP transactions through kotlin-logging. By default the
filter logs:
- Only HTTP transactions with error response statuses (4xx, 5xx)
- All routes/paths
- All headers
This can be configured to log all HTTP transactions, ignore certain URI paths or exclude headers from being logged.
You can also attach extra fields by providing an extraFields lambda
with an HttpTransaction argument and returning
a Map<String, String>.
val handler = HttpAccessLog(
allStatues = true,
ignorePaths = setOf("/admin"),
excludeHeaders = setOf("jwt-token"),
extraFields = { httpTransaction ->
mapOf("remoteIp" to (httpTransaction.request.header("X-Forwarded-For") ?: ""))
}
).then(routes)
Caching
cacheDays(days) and cacheOneYear() are thin wrappers over http4k's caching filter, primarily intended
for the static assets routes.
"/static/${assetsVersion}" bind cacheDays(365).then(staticAssets(developmentMode))