There is no single answer to this problem. There are multiple subtle considerations here. My answer will attempt to provide some direction.
At least four different cases to handle:
- Application level valid results (connection established, response received, 200 status code)
- Application level errors (connection established, response received, 4xx, 5xx status code)
- Networking IO errors (connection not established, or no response received due to timeout etc.)
- JSON parsing errors (connection established, response received, failed to convert JSON to model domain object)
Pseudocode:
Completed Future
with response inside which is either ErrorResponse
or MyResponseClass
, that is, Either[ErrorResponse, MyResponseClass]
:
- If service returns 200 status code, then parse as
MyResponseClass
- If service returns >= 400 status code, then parse as
ErrorResponse
Completed Future
with exception inside:
- Parsing Exception, or
- Networking IO Exception (for example timeout)
Future(Left(errorResponse))
vs. Future(throw new Exception)
Note the difference between Future(Left(errorResponse))
and Future(throw new Exception)
: we consider only the latter as a failed future. The former, despite having a Left
inside is still consider a successfully completed future.
Future.andThen
vs Future.recover
Note the difference between Future.andThen
and Future.recover
: former does not alter the value inside the future, while the latter can alter the value inside and its type. If recovery is impossible we could at least log exceptions using andThen
.
Example:
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import play.api.libs.ws._
import play.api.libs.ws.ahc._
import scala.concurrent.ExecutionContext.Implicits._
import scala.concurrent.Future
import play.api.libs.json._
import play.api.libs.ws.JsonBodyReadables._
import scala.util.Failure
import java.io.IOException
import com.fasterxml.jackson.core.JsonParseException
case class ErrorMessage(message: String)
object ErrorMessage {
implicit val errorMessageFormat = Json.format[ErrorMessage]
}
case class ErrorResponse(error: List[ErrorMessage])
object ErrorResponse {
implicit val errorResponseFormat = Json.format[ErrorResponse]
}
case class MyResponseClass(a: String, b: String)
object MyResponseClass {
implicit val myResponseClassFormat = Json.format[MyResponseClass]
}
object PlayWsErrorHandling extends App {
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
val wsClient = StandaloneAhcWSClient()
httpRequest(wsClient) map {
case Left(errorResponse) =>
println(s"handle application level error: $errorResponse")
// ...
case Right(goodResponse) =>
println(s"handle application level good response $goodResponse")
// ...
} recover { // handle failed futures (futures with exceptions inside)
case parsingError: JsonParseException =>
println(s"Attempt recovery from parsingError")
// ...
case networkingError: IOException =>
println(s"Attempt recovery from networkingError")
// ...
}
def httpRequest(wsClient: StandaloneWSClient): Future[Either[ErrorResponse, MyResponseClass]] =
wsClient.url("http://www.example.com").get() map { response ⇒
if (response.status >= 400) // application level error
Left(response.body[JsValue].as[ErrorResponse])
else // application level good response
Right(response.body[JsValue].as[MyResponseClass])
} andThen { // exceptions thrown inside Future
case Failure(exception) => exception match {
case parsingError: JsonParseException => println(s"Log parsing error: $parsingError")
case networkingError: IOException => println(s"Log networking errors: $networkingError")
}
}
}
Dependencies:
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-ahc-ws-standalone" % "1.1.3",
"com.typesafe.play" %% "play-ws-standalone-json" % "1.1.3"
)