Exception Handling
Overview
ExceptionHandler is a PartialFunction[Throwable, Route]. It intercepts exceptions thrown during route evaluation and converts them into HTTP responses. If no custom handler is provided, unhandled non-fatal exceptions result in a 500 Internal Server Error response.
Creating a Custom Handler
Construct an ExceptionHandler by passing a partial function to ExceptionHandler.apply:
import pl.iterators.stir.server._
import pl.iterators.stir.server.Directives._
import org.http4s.Status
val myExceptionHandler = ExceptionHandler {
case _: ArithmeticException =>
complete(Status.BadRequest, "Arithmetic error")
case _: IllegalArgumentException =>
complete(Status.BadRequest, "Invalid argument")
}
The partial function matches on exception types and returns a Route that produces the desired error response.
Chaining Handlers with withFallback
Combine two handlers so that the second acts as a fallback when the first does not match:
val combined = primaryHandler.withFallback(secondaryHandler)
If primaryHandler does not handle a given exception, secondaryHandler is consulted. A handler that has already been sealed (see below) will not accept a fallback, since it already handles all non-fatal exceptions.
Sealing a Handler
The seal method attaches the default handler as a fallback, ensuring all non-fatal exceptions are covered:
val sealed = myExceptionHandler.seal()
An optional logAction parameter allows customizing how exceptions are logged:
import cats.effect.IO
val sealed = myExceptionHandler.seal(
logAction = Some((throwable, message) => IO.println(message))
)
Sealing an already sealed handler returns it unchanged, so a log action given the second time is ignored, as in Pekko.
Route.seal takes no log action; to log through your own action, wrap the route in
handleExceptions(ExceptionHandler.seal(handler)(logAction)) before sealing it.
Default Behavior
The default ExceptionHandler (returned by ExceptionHandler.default()) handles all non-fatal exceptions by:
IllegalRequestException(status, message)(raised for example bytoStrictEntitywhen the entity does not arrive in time) completes with the given status and message.EntityStreamSizeException(raised bytoStrictEntityandwithSizeLimitwhen the entity is too large) completes with413 Payload Too Large.- Any other non-fatal exception is logged with its stack trace to stderr and completes with
500 Internal Server Error.
The handleExceptions Directive
Apply a custom ExceptionHandler to an inner route using the handleExceptions directive from ExecutionDirectives:
val route =
handleExceptions(myExceptionHandler) {
path("divide") {
complete((1 / 0).toString)
}
}
Exceptions thrown during evaluation of the inner route -- whether thrown synchronously or produced by a failed IO -- are caught and processed by the provided handler. If the handler does not match the exception, it propagates outward.
Relationship with Route.seal
Route.seal applies both a RejectionHandler and an ExceptionHandler to a route. It accepts an implicit ExceptionHandler parameter:
implicit val myHandler: ExceptionHandler = myExceptionHandler
val sealedRoute = Route.seal(route)
If no implicit ExceptionHandler is in scope, Route.seal uses the default handler. The ExceptionHandler.seal companion method handles null safely by falling back to ExceptionHandler.default().
When converting a route to an HttpApp[IO] via .toHttpApp, the route is automatically sealed with the default exception and rejection handlers.