Skip to content

Handlers & Results

Every request processor in Apion — routes, middleware, and error handlers — uses a single type:

type Handler = Request => Future[Result]

A handler returns one of four results:

sealed trait Result
case class Continue(request: Request) extends Result
case class Complete(response: Response) extends Result
case class Fail(error: ServerError) extends Result
case object Skip extends Result

Pass a (possibly modified) request to the next handler. This is how middleware works — it transforms the request and passes it along:

val addTimestamp: Handler = request =>
Future.successful(Continue(
request.copy(context = request.context + ("startTime" -> System.currentTimeMillis()))
))

End the handler chain and send a response to the client:

val hello: Handler = _ =>
Future.successful(Complete(Response.text("Hello World")))
// Or using the DSL:
val hello: Handler = _ => "Hello World".asText

Propagate a typed error. The error flows to the next registered error handler:

val requireId: Handler = request =>
request.params.get("id") match {
case Some(id) => Future.successful(Continue(request))
case None => failValidation("Missing id parameter")
}

Skip this handler and try the next one. Useful in error handlers that only handle specific error types:

server.use { (error: ServerError, request: Request) =>
error match {
case e: ValidationError => Map("error" -> e.message).asJson(400)
case _ => skip // Let the next error handler deal with it
}
}
  1. The server receives an HTTP request and creates an immutable Request
  2. Handlers execute sequentially (middleware first, then routes)
  3. Each handler returns a Result that determines what happens next
  4. When a handler returns Complete, response finalizers run in LIFO order
  5. The response is sent to the client

Apion provides three built-in error types:

trait ServerError extends Throwable
case class ValidationError(message: String) extends ServerError
case class AuthError(message: String) extends ServerError
case class NotFoundError(message: String) extends ServerError

Create errors using the DSL helpers:

failValidation("Invalid input") // Future[Fail(ValidationError(...))]
failAuth("Unauthorized") // Future[Fail(AuthError(...))]
failNotFound("Not found") // Future[Fail(NotFoundError(...))]
fail(CustomError("oops")) // Future[Fail(yourError)]

Define domain-specific errors by extending ServerError:

case class RateLimitError(message: String, retryAfter: Long) extends ServerError
case class ConflictError(message: String) extends ServerError

Error handlers receive both the error and the original request:

server.use { (error: ServerError, request: Request) =>
error match {
case ValidationError(msg) =>
Map("error" -> "validation", "message" -> msg).asJson(400)
case AuthError(msg) =>
Map("error" -> "auth", "message" -> msg).asJson(401)
case NotFoundError(msg) =>
Map("error" -> "not_found", "message" -> msg).asJson(404)
case _ =>
Map("error" -> "internal").asJson(500)
}
}

Error handlers can also transform errors:

server.use { (error: ServerError, _: Request) =>
error match {
case e: ValidationError =>
Future.successful(Fail(CustomError(s"Validation failed: ${e.message}")))
case _ => skip
}
}

Pass multiple handlers to a route — they execute sequentially:

server.get("/admin/users",
authMiddleware, // Verify JWT
requireAdmin, // Check admin role
listUsersHandler // Return data
)

Apply a handler to all routes:

server
.use(LoggingMiddleware())
.use(CorsMiddleware())

Apply a handler only to routes matching a prefix:

server.use("/api", authMiddleware)

Finalizers transform a response after a handler returns Complete. They execute in LIFO (last-in, first-out) order:

type Finalizer = (Request, Response) => Future[Response]

Middleware adds finalizers via request.addFinalizer:

val timing: Handler = request => {
val start = System.currentTimeMillis()
val finalizer: Finalizer = (_, response) => {
val duration = System.currentTimeMillis() - start
Future.successful(response.copy(
headers = response.headers.add("X-Response-Time", s"${duration}ms")
))
}
Future.successful(Continue(request.addFinalizer(finalizer)))
}

This pattern is used by CompressionMiddleware, LoggingMiddleware, and CookieMiddleware to inspect or modify responses without breaking the linear handler flow.