Errors, failures: I’m a teapot

Imagine you want to use the HTTP status codes to indicate failures in your Akka & Spray application. You receive a (JSON) request that maps to known route and can be unmarshalled into an instance of your object. But alas! validation fails. The response should indicate that the request was not OK (200), but you want to send some body that includes the details of the error.

Luckily, nothing is easier. We already have a good mechanism of passing bad and good values. The bad guys appear on the left projection of Either, the good guys appear on the right projection. Translating our baby-speak to Scala, we have Either[A, B], where the value of type A indicates error; and the value of type B indicates success. (Think that the value on the right is—erm—right.)

So, the task is to have be able to wire in some error resolution to the Spray routes.

class UserService(implicit val actorSystem: ActorSystem) 
  extends Directives ... {

  def userActor = actorSystem.actorFor("/user/application/user")

  import ExecutionContext.Implicits.global

  val route = {
    path("users" / "register") {
      post {
        handleWith { registerUser: RegisterUser =>
          (userActor ? registerUser).mapTo[
            Either[NotRegisteredUser, RegisteredUser]
        }
      }
    } 
}

We would like to send back HTTP status 200 when the userActor replies to our message RegisterUser with value on the right, and whatever the ErrorSelector for the bad value A returns when we get the value on the left. So, we need a marshaller that understands the errors.

trait Marshalling extends DefaultJsonProtocol with SprayJsonSupport 
  with MetaMarshallers {
  type ErrorSelector[A] = A => StatusCode

  implicit def errorSelectingEitherMarshaller[A, B]
    (implicit ma: Marshaller[A], mb: Marshaller[B], esa: ErrorSelector[A]) =
    Marshaller[Either[A, B]] { (value, ctx) =>
      value match {
        case Left(a) =>
          val mc = new CollectingMarshallingContext()
          ma(a, mc)
          ctx.handleError(ErrorResponseException(esa(a), mc.entity))
        case Right(b) =>
          mb(b, ctx)
      }
    }

}

case class ErrorResponseException(responseStatus: StatusCode, 
                                  response: Option[HttpEntity]) 
                                  extends RuntimeException

Now that we have the errorSelectingEitherMarshaller available, we need to sellotape the rest of the code together. We need to be able to handle our ErrorResponseException globally, for all routes. Enter RoutedHttpService.

class RoutedHttpService(route: Route) extends Actor with HttpService {

  implicit def actorRefFactory = context

  implicit val handler = ExceptionHandler.fromPF {
    case NonFatal(ErrorResponseException(statusCode, entity)) 
      => log => ctx =>
        ctx.complete(statusCode, entity)

    case NonFatal(e) => log => ctx =>
      log.error(e, "Error during processing of request {}", ctx.request)
      ctx.complete(InternalServerError)
    }


  def receive = {
    runRoute(route)(handler, RejectionHandler.Default, context, 
                    RoutingSettings.Default)
  }

}

Now that all the mechanics is wired together, we’re free to implement the instances of the ErrorSelector[A] for different As or take advantage of the fact that (by convention) all our errors extend ApplicationFailure and thus naively implement the ApplicaitonFailureErrorSelector:

implicit object ApplicationFailurErrorSelector  
  extends ErrorSelector[ApplicationFailure] {
  def apply(v1: ApplicationFailure) = StatusCodes.UnprocessableEntity
}

Summary

With the appropriate instance of ErrorSelector for A, the errorSelectingEitherMarshaller[A, B], Marshaller for both A and B, we can correctly deal with errors: return appropriate HTTP status codes as well as the responses for those errors. For the complete code, head over to https://github.com/janm399/akka-patterns.

This entry was posted in Jan's Blog and tagged , , . Bookmark the permalink.

One Response to Errors, failures: I’m a teapot

  1. Pingback: This week in #Scala (14/12/2012) | Cake Solutions Team Blog

Leave a Reply