Error Handling

In the previous section, we saw how we can decode responses automatically to an algebraic data type using send[T]. We also saw how the decoded response is wrapped in a Validated data type, to capture any failures in decoding the response.

Requests can fail for other reasons besides malformed responses, though. What happens if the server returns an HTTP error, like 404 Not Found or 401 Unauthorized? Let’s set up another dummy server that returns errors, so we can explore the various ways of handling them:

import com.twitter.util.{Future,Await}
// import com.twitter.util.{Future, Await}

import com.twitter.finagle.{Service,Http}
// import com.twitter.finagle.{Service, Http}

import com.twitter.finagle.http.{Request,Response,Status,Version}
// import com.twitter.finagle.http.{Request, Response, Status, Version}

import java.net.{URL, InetSocketAddress}
// import java.net.{URL, InetSocketAddress}

import featherbed.request.ErrorResponse
// import featherbed.request.ErrorResponse

import featherbed.circe._
// import featherbed.circe._

import io.circe.generic.auto._
// import io.circe.generic.auto._

val server = Http.serve(new InetSocketAddress(8768), new Service[Request, Response] {
  def response(status: Status, content: String) = {
    val rep = Response(Version.Http11, status)
    rep.contentString = content
    rep.contentType = "application/json"
    Future.value(rep)
  }

  def apply(request: Request): Future[Response] = request.uri match {
    case "/api/success" => response(Status.Ok, """{"foo": "bar"}""")
    case "/api/not/found" => response(
        Status.NotFound,
        """{"error": "The thing couldn't be found"}"""
      )
    case "/api/unauthorized" => response(
        Status.Unauthorized,
        """{"error": "Not authorized to access the thing"}"""
      )
    case "/api/server/error" => response(
        Status.InternalServerError,
        """{"error": "Something went terribly wrong"}"""
      )
  }
})
// server: com.twitter.finagle.ListeningServer = com.twitter.finagle.server.ListeningStackServer$$anon$1@4a4cc814

// the type of the successful response
case class Foo(foo: String)
// defined class Foo

// the client
val client = new featherbed.Client(new URL("http://localhost:8768/api/"))
// client: featherbed.Client = featherbed.Client@52909c06

When using the send[T] method, the resulting Future will fail if the server returns an HTTP error. This means that in order to handle an error, you must handle it at the Future level using the Future API:

val req = client.get("not/found").accept("application/json")
// req: client.GetRequest[String("application/json") :+: shapeless.CNil] = GetRequest(http://localhost:8768/api/not/found,List(),UTF-8)

Await.result {
  req.send[Foo]().handle {
    case ErrorResponse(request, response) =>
      throw new Exception(s"Error response $response to request $request")
  }
}
// java.lang.Exception: Error response Response("HTTP/1.1 Status(404)") to request Request("GET /api/not/found", from 0.0.0.0/0.0.0.0:0)
//   at $anonfun$1.applyOrElse(<console>:31)
//   at $anonfun$1.applyOrElse(<console>:29)
//   at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:34)
//   at com.twitter.util.Future$$anonfun$handle$1.$anonfun$applyOrElse$1(Future.scala:1239)
//   at com.twitter.util.Try$.apply(Try.scala:15)
//   at com.twitter.util.Future$.apply(Future.scala:163)
//   at com.twitter.util.Future$$anonfun$handle$1.applyOrElse(Future.scala:1239)
//   at com.twitter.util.Future$$anonfun$handle$1.applyOrElse(Future.scala:1238)
//   at com.twitter.util.Future.$anonfun$rescue$1(Future.scala:1119)
//   at com.twitter.util.Promise$Transformer.liftedTree1$1(Promise.scala:107)
//   at com.twitter.util.Promise$Transformer.k(Promise.scala:107)
//   at com.twitter.util.Promise$Transformer.apply(Promise.scala:117)
//   at com.twitter.util.Promise$Transformer.apply(Promise.scala:98)
//   at com.twitter.util.Promise$$anon$1.run(Promise.scala:421)
//   at com.twitter.concurrent.LocalScheduler$Activation.run(Scheduler.scala:200)
//   at com.twitter.concurrent.LocalScheduler$Activation.submit(Scheduler.scala:158)
//   at com.twitter.concurrent.LocalScheduler.submit(Scheduler.scala:272)
//   at com.twitter.concurrent.Scheduler$.submit(Scheduler.scala:108)
//   at com.twitter.util.Promise.runq(Promise.scala:406)
//   at com.twitter.util.Promise.updateIfEmpty(Promise.scala:801)
//   at com.twitter.util.Promise.update(Promise.scala:775)
//   at com.twitter.util.Promise.setValue(Promise.scala:751)
//   at com.twitter.concurrent.AsyncQueue.offer(AsyncQueue.scala:123)
//   at com.twitter.finagle.netty3.transport.ChannelTransport.handleUpstream(ChannelTransport.scala:56)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
//   at org.jboss.netty.channel.DefaultChannelPipeline$DefaultChannelHandlerContext.sendUpstream(DefaultChannelPipeline.java:791)
//   at org.jboss.netty.handler.codec.http.HttpContentDecoder.messageReceived(HttpContentDecoder.java:108)
//   at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:70)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
//   at org.jboss.netty.channel.DefaultChannelPipeline$DefaultChannelHandlerContext.sendUpstream(DefaultChannelPipeline.java:791)
//   at org.jboss.netty.channel.SimpleChannelUpstreamHandler.messageReceived(SimpleChannelUpstreamHandler.java:124)
//   at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:70)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
//   at org.jboss.netty.channel.DefaultChannelPipeline$DefaultChannelHandlerContext.sendUpstream(DefaultChannelPipeline.java:791)
//   at org.jboss.netty.handler.codec.http.HttpChunkAggregator.messageReceived(HttpChunkAggregator.java:145)
//   at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:70)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
//   at org.jboss.netty.channel.DefaultChannelPipeline$DefaultChannelHandlerContext.sendUpstream(DefaultChannelPipeline.java:791)
//   at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:296)
//   at org.jboss.netty.handler.codec.frame.FrameDecoder.unfoldAndFireMessageReceived(FrameDecoder.java:459)
//   at org.jboss.netty.handler.codec.replay.ReplayingDecoder.callDecode(ReplayingDecoder.java:536)
//   at org.jboss.netty.handler.codec.replay.ReplayingDecoder.messageReceived(ReplayingDecoder.java:435)
//   at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:70)
//   at org.jboss.netty.handler.codec.http.HttpClientCodec.handleUpstream(HttpClientCodec.java:92)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
//   at org.jboss.netty.channel.DefaultChannelPipeline$DefaultChannelHandlerContext.sendUpstream(DefaultChannelPipeline.java:791)
//   at org.jboss.netty.channel.SimpleChannelHandler.messageReceived(SimpleChannelHandler.java:142)
//   at com.twitter.finagle.netty3.channel.ChannelStatsHandler.messageReceived(ChannelStatsHandler.scala:68)
//   at org.jboss.netty.channel.SimpleChannelHandler.handleUpstream(SimpleChannelHandler.java:88)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
//   at org.jboss.netty.channel.DefaultChannelPipeline$DefaultChannelHandlerContext.sendUpstream(DefaultChannelPipeline.java:791)
//   at org.jboss.netty.channel.SimpleChannelHandler.messageReceived(SimpleChannelHandler.java:142)
//   at com.twitter.finagle.netty3.channel.ChannelRequestStatsHandler.messageReceived(ChannelRequestStatsHandler.scala:32)
//   at org.jboss.netty.channel.SimpleChannelHandler.handleUpstream(SimpleChannelHandler.java:88)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
//   at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)
//   at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:268)
//   at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255)
//   at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)
//   at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:108)
//   at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:337)
//   at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:89)
//   at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
//   at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
//   at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
//   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
//   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
//   at com.twitter.finagle.util.BlockingTimeTrackingThreadFactory$$anon$1.run(BlockingTimeTrackingThreadFactory.scala:24)
//   at java.lang.Thread.run(Thread.java:745)

This isn’t a very useful error handler, but it demonstrates how errors can be intercepted at the Future level. The handle or rescue methods of Future can be used to recover from the failure. See their API docs for more information. The exception that’s returned in a Future which failed due to a server error response is of type ErrorResponse, which contains the request and response.

Often, the a REST API will be set up to return some meaningful representation of errors in the same content type as its responses. In the example above, our dummy server is set up to return JSON errors in a well-defined structure. To capture this, we can use the send[Error, Success] method instead of send[T]:

// ADT for errors
case class Error(error: String)
// defined class Error

val req = client.get("not/found").accept("application/json")
// req: client.GetRequest[String("application/json") :+: shapeless.CNil] = GetRequest(http://localhost:8768/api/not/found,List(),UTF-8)

Await.result(req.send[Error, Foo])
// res4: Either[Error,Foo] = Left(Error(The thing couldn't be found))

Instead of an exception, we’re capturing the server errors in an Either[Error, Foo]. This is a typical pattern in Scala functional programming for dealing with operations which may fail. The benefit is that the well-defined error type is also automatically decoded for us. However, if the error can’t be decoded, this will still result in a failed Future, which fails on the decoding rather than the server error.

Next, read about Building REST Clients