Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Processing JSON error responses with Play WSClient

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])
like image 350
tgk Avatar asked Mar 08 '23 15:03

tgk


1 Answers

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"
)
like image 161
Mario Galic Avatar answered Mar 10 '23 04:03

Mario Galic