Skip to main content

Rejection Handling

The Rejection Model​

When a directive cannot handle a request, it rejects the request by producing one or more Rejection values rather than completing with a response. Rejections accumulate across route alternatives composed with ~ (or concat). If no alternative route handles the request, the collected rejections are processed by the RejectionHandler to produce an appropriate HTTP response.

For example, a get directive rejects non-GET requests with a MethodRejection. If the route structure offers multiple alternatives, each alternative may add its own rejections. TransformationRejection values are used internally to cancel out redundant rejections (e.g., multiple MethodRejection values when the method did match in one branch).

val route =
path("order") {
get {
complete("Received GET")
} ~
post {
complete("Received POST")
}
}

A PUT /order request would be rejected by both the get and post directives. The resulting rejections are collected and passed to the RejectionHandler, which produces a 405 Method Not Allowed response.

Built-in Rejection Types​

RejectionDescription
MethodRejection(supported)The HTTP method is not supported. Contains the Method that the rejecting directive supports.
SchemeRejection(supported)The URI scheme is not supported. Contains the scheme string the directive expects.
MissingQueryParamRejection(parameterName)A required query parameter was not found.
MalformedQueryParamRejection(parameterName, errorMsg, cause)A query parameter was present but could not be interpreted.
InvalidRequiredValueForQueryParamRejection(parameterName, expectedValue, actualValue)A query parameter value did not match the required value.
MissingHeaderRejection(headerName)A required HTTP header was not found.
MalformedHeaderRejection(headerName, errorMsg, cause)An HTTP header value was malformed.
MissingCookieRejection(cookieName)A required cookie was not found.
MissingFormFieldRejection(fieldName)A required form field was not found.
MalformedFormFieldRejection(fieldName, errorMsg, cause)A form field could not be interpreted.
UnsupportedRequestContentTypeRejection(supported, contentType)The request's Content-Type is not accepted by the entity decoder. Produces 415.
MalformedRequestContentRejection(message, cause)Unmarshalling the request content failed with a non-IllegalArgumentException error. Produces 400, or 413 when the cause is an EntityStreamSizeException.
RequestEntityExpectedRejectionAn entity was expected but the request has none. Produced by requestEntityPresent, and by entity and the disk-based upload directives when the decoder fails on a request known to have no body (Content-Length: 0, or an HTTP/1.x request with neither a length nor chunking). Produces 400.
InvalidOriginRejection(allowedOrigins)The Origin header is not in the allowed list. Produced by checkSameOrigin. Produces 403.
UnacceptedResponseContentTypeRejection(supported)The client accepts none of the content types the route can produce (Accept). Produced by complete for EntityEncoder-based marshallers. Produces 406 listing the types.
UnacceptedResponseEncodingRejection(supported)The client accepts none of the response encodings the route can produce. Produced by encodeResponse* and responseEncodingAccepted. Produces 406.
UnsupportedRequestEncodingRejection(supported)The request's Content-Encoding is not supported. Produced by decodeRequest* and requestEncodedWith. Produces 400.
MissingAttributeRejection(key)A request attribute was expected but missing. Produced by attribute. Produces 500.
UnsatisfiableRangeRejection(unsatisfiableRanges, actualEntityLength)None of the requested byte ranges fits the entity, or the response has no Content-Length (then actualEntityLength is -1). Produced by withRangeSupport. Produces 416 with Content-Range: bytes */length, omitted when the length is unknown.
TooManyRangesRejection(maxRanges)The request asked for more byte ranges than allowed. Produced by withRangeSupport. Produces 416.
ExpectedWebSocketRequestRejectionThe request is not a WebSocket upgrade. Produced by the WebSocket directives. Produces 400.
UnsupportedWebSocketSubprotocolRejection(supportedProtocol)None of the offered WebSocket subprotocols is supported. Produces 400 with a Sec-WebSocket-Protocol header listing the supported ones.
ValidationRejection(message, cause)An expected value was semantically invalid. Produced by the validate directive and by IllegalArgumentException from domain constructors.
AuthenticationFailedRejection(cause, challenge)Authentication failed. The cause is either CredentialsMissing or CredentialsRejected.
AuthorizationFailedRejectionThe authenticated user is not authorized to access the resource.
TransformationRejection(transform)A special rejection that carries a transformation function to cancel or modify other rejections. Used internally by directives.

Building a Custom RejectionHandler​

Use RejectionHandler.newBuilder() to construct a handler:

import pl.iterators.stir.server._
import pl.iterators.stir.server.Directives._
import org.http4s.Status

val myRejectionHandler = RejectionHandler.newBuilder()
.handle { case MissingQueryParamRejection(param) =>
complete(Status.BadRequest, s"Missing parameter: $param")
}
.handleAll[MethodRejection] { rejections =>
val methods = rejections.map(_.supported.name).mkString(", ")
complete(Status.MethodNotAllowed, s"Supported methods: $methods")
}
.handleNotFound {
complete(Status.NotFound, "Resource not found")
}
.result()

Builder Methods​

  • .handle { case SomeRejection => route } -- Handle a specific rejection type by pattern matching. The first matching handler wins.
  • .handleAll[T] { rejections => route } -- Handle all rejections of type T at once. The rejections sequence is guaranteed to be non-empty.
  • .handleNotFound { route } -- Handle the case when no route matched the request (empty rejection list).
  • .result() -- Build and return the immutable RejectionHandler.

Transforming Rejection Responses​

The mapRejectionResponse method transforms the HTTP response produced by a RejectionHandler. It can only be called on a built handler (one returned by .result()).

val handler = RejectionHandler.newBuilder()
.handleNotFound {
complete(Status.NotFound, "Not here")
}
.result()
.mapRejectionResponse { response =>
response.putHeaders(org.http4s.headers.`Cache-Control`(
org.http4s.CacheDirective.`no-cache`()
))
}

Chaining Handlers with withFallback​

Combine two handlers so that the second acts as a fallback when the first does not handle a rejection:

val combined = primaryHandler.withFallback(secondaryHandler)

If the primary handler does not match a rejection, the secondary handler is consulted.

The handleRejections Directive​

Apply a custom RejectionHandler to the inner route using the handleRejections directive from ExecutionDirectives:

val route =
handleRejections(myRejectionHandler) {
path("order") {
get {
complete("order")
}
}
}

Rejections produced by the inner route are intercepted and processed by the provided handler. If the handler does not cover a rejection, it propagates outward.

Route Sealing​

Route.seal wraps a route with default rejection handling and exception handling. A sealed route will never produce unhandled rejections; instead, they are converted to appropriate HTTP error responses.

val sealedRoute = Route.seal(route)

Route.seal accepts implicit RejectionHandler and ExceptionHandler parameters, defaulting to RejectionHandler.default and the default ExceptionHandler:

implicit val myHandler: RejectionHandler = myRejectionHandler
val sealedRoute = Route.seal(route) // uses myHandler

The default RejectionHandler maps all built-in rejection types to appropriate HTTP status codes and error messages. Calling .seal on a RejectionHandler attaches RejectionHandler.default as a fallback, ensuring all rejections are covered.