4

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])
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
tgk
  • 3,857
  • 2
  • 27
  • 42
  • 1
    You could use something like the following: Try(response.json).map(jsValue => jsValue.validate[OrderDto] match { case JsSuccess(orderDto, _) => println(orderDto) case JsError(errors) => println(errors) } ) This will wrap the exception in a Try if the response is not a json. Otherwise it will either print out your dto or the validation errors. – okarahan Nov 12 '17 at 15:40

2 Answers2

5

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:

  1. Application level valid results (connection established, response received, 200 status code)
  2. Application level errors (connection established, response received, 4xx, 5xx status code)
  3. Networking IO errors (connection not established, or no response received due to timeout etc.)
  4. JSON parsing errors (connection established, response received, failed to convert JSON to model domain object)

Pseudocode:

  1. Completed Future with response inside which is either ErrorResponse or MyResponseClass, that is, Either[ErrorResponse, MyResponseClass]:

    1. If service returns 200 status code, then parse as MyResponseClass
    2. If service returns >= 400 status code, then parse as ErrorResponse
  2. Completed Future with exception inside:

    1. Parsing Exception, or
    2. 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"
)
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
2

There is the Either[L, R] which is perfect for the cases where you could have a value or another (not both, neither none), you could do something like this (not tested):

val result: Option[Either[ErrorClass, MyResponseClass]] = response
  .json
  .validateOpt[MyResponseClass]
  .map { resp => Right(resp) }
  .orElse {
    response.json.validateOpt[ErrorClass]
      .map { error => Left(error) }
  }

it's a common pattern to store the error result in the left side and the success in the right side.

AlexITC
  • 1,054
  • 1
  • 10
  • 21