使用Play WSClient处理JSON错误响应

时间:2017-11-09 20:03:03

标签: scala playframework play-json ws-client

我使用Play的WSClient与第三方服务进行互动

request = ws.url(baseUrl)
  .post(data)
  .map{ response =>
     response.json.validate[MyResponseClass]

回复可能是MyResponseClass,也可能是ErrorResponse,如{ "error": [ { "message": "Error message" } ] }

是否有一种典型的解析类错误的方法?

我应该这样做吗?

response.json.validateOpt[MyResponseClass].getOrElse(response.json.validateOpt[ErrorClass])

2 个答案:

答案 0 :(得分:3)

这个问题没有单一的答案。这里有许多微妙的考虑因素。我的回答将尝试提供一些方向。

至少要处理四种不同的案例

  1. 应用程序级别有效结果(建立连接,收到响应,200状态代码)
  2. 应用程序级错误(建立连接,收到响应,4xx,5xx状态代码)
  3. 网络IO错误(未建立连接,或因超时等原因未收到响应)
  4. JSON解析错误(建立连接,收到响应,无法将JSON转换为模型域对象)
  5. 伪代码

    1. 已完成Future,其回复位于ErrorResponseMyResponseClass,即Either[ErrorResponse, MyResponseClass]

      1. 如果服务返回200状态代码,则解析为MyResponseClass
      2. 如果服务返回> = 400状态代码,则解析为ErrorResponse
    2. 已完成Future,但内部有例外:

      1. 解析异常,或
      2. 网络IO异常(例如超时)
    3. Future(Left(errorResponse))Future(throw new Exception)

      请注意Future(Left(errorResponse))Future(throw new Exception)之间的区别:我们只将后者视为failed future。前者虽然内部有Left,但仍然认为是一个成功完成的未来。

      Future.andThen vs Future.recover

      注意Future.andThenFuture.recover之间的区别:前者不会改变未来的值,而后者可以改变内部的值及其类型。如果无法恢复,我们至少可以使用andThen来记录异常。

      示例

      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")
            }
          }
      }
      

      依赖关系:

      libraryDependencies ++= Seq(
        "com.typesafe.play" %% "play-ahc-ws-standalone"   % "1.1.3",
        "com.typesafe.play" %% "play-ws-standalone-json"  % "1.1.3"
      )
      

答案 1 :(得分:1)

Either[L, R]适用于您可以拥有某个值或另一个值的情况(不是两者都有),您可以执行此类操作(未经测试):

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

它是将错误结果存储在左侧并且成功存储在右侧的常见模式。