Rejections

In the chapter about constructing Routes the ~ operator was introduced, which connects two routes in a way that allows a second route to get a go at a request if the first route “rejected” it. The concept of “rejections” is used by spray-routing for maintaining a more functional overall architecture and in order to be able to properly handle all kinds of error scenarios.

When a filtering directive, like the get directive, cannot let the request pass through to its inner Route because the filter condition is not satisfied (e.g. because the incoming request is not a GET request) the directive doesn’t immediately complete the request with an error response. Doing so would make it impossible for other routes chained in after the failing filter to get a chance to handle the request. Rather, failing filters “reject” the request in the same way as by explicitly calling requestContext.reject(...).

After having been rejected by a route the request will continue to flow through the routing structure and possibly find another route that can complete it. If there are more rejections all of them will be picked up and collected.

If the request cannot be completed by (a branch of) the route structure an enclosing handleRejections directive can be used to convert a set of rejections into an HttpResponse (which, in most cases, will be an error response). The runRoute Wrapper defined by the The HttpService trait internally wraps its argument route with the handleRejections directive in order to “catch” and handle any rejection.

Predefined Rejections

A rejection encapsulates a specific reason why a Route was not able to handle a request. It is modeled as an object of type Rejection. spray-routing comes with a set of predefined rejections, which are used by various predefined directives.

Rejections are gathered up over the course of a Route evaluation and finally converted to HttpResponse replies by the handleRejections directive if there was no way for the request to be completed.

RejectionHandler

The handleRejections directive delegates the actual job of converting a list of rejections to its argument, a RejectionHandler, which is defined like this:

trait RejectionHandler extends PartialFunction[List[Rejection], Route]

Since a RejectionHandler is a partial function it can choose, which rejections it would like to handle and which not. Unhandled rejections will simply continue to flow through the route structure. The top-most RejectionHandler applied by The runRoute Wrapper will handle all rejections that reach it.

So, if you’d like to customize the way certain rejections are handled simply bring a custom RejectionHandler into implicit scope of The runRoute Wrapper or pass it to an explicit handleRejections directive that you have put somewhere into your route structure.

Here is an example:

import spray.routing._
import spray.http._
import StatusCodes._
import Directives._

implicit val myRejectionHandler = RejectionHandler {
  case MissingCookieRejection(cookieName) :: _ =>
    complete(BadRequest, "No cookies, no service!!!")
}

class MyService extends HttpServiceActor {
  def receive = runRoute {
    `<my-route-definition>`
  }
}

Rejection Cancellation

As you can see from its definition above the RejectionHandler handles not single rejections but a whole list of them. This is because some route structure produce several “reasons” why a request could not be handled.

Take this route structure for example:

import spray.httpx.encoding._

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

For uncompressed POST requests this route structure could yield two rejections:

  • a MethodRejection produced by the get directive (which rejected because the request is not a GET request)
  • an UnsupportedRequestEncodingRejection produced by the decodeRequest directive (which only accepts gzip-compressed requests)

In reality the route even generates one more rejection, a TransformationRejection produced by the post directive. It “cancels” all other potentially existing MethodRejections, since they are invalid after the post directive allowed the request to pass (after all, the route structure can deal with POST requests). These types of rejection cancellations are resolved before a RejectionHandler sees the rejection list. So, for the example above the RejectionHandler will be presented with only a single-element rejection list, containing nothing but the UnsupportedRequestEncodingRejection.

Empty Rejections

Since rejections are passed around in lists you might ask yourself what the semantics of an empty rejection list are. In fact, empty rejection lists have well defined semantics. They signal that a request was not handled because the respective resource could not be found. spray-routing reserves the special status of “empty rejection” to this most common failure a service is likely to produce.

So, for example, if the path directive rejects a request, it does so with an empty rejection list. The host directive behaves in the same way.