I'm using Play's WSClient to interact with a third-party service
request = ws.url(baseUrl)
.post(data)
.map{ response =>
response.json.validate[MyResponseClass]
The response may be a MyResponseClass
or it may be an ErrorResponse
like { "error": [ { "message": "Error message" } ] }
Is there a typical way to parse either the Class or the Error?
Should i do something like this?
response.json.validateOpt[MyResponseClass].getOrElse(response.json.validateOpt[ErrorClass])
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:
Pseudocode:
Completed Future
with response inside which is either ErrorResponse
or MyResponseClass
, that is, Either[ErrorResponse, MyResponseClass]
:
MyResponseClass
ErrorResponse
Completed Future
with exception inside:
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"
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With