Обработка ответов об ошибках JSON с помощью Play WSClient

Я использую 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])

person tgk    schedule 09.11.2017    source источник
comment
Вы можете использовать что-то вроде следующего: errors) } ) Это перенесет исключение в Try, если ответ не является json. В противном случае он либо распечатает ваш dto, либо ошибки проверки.   -  person okarahan    schedule 12.11.2017


Ответы (2)


Однозначного ответа на эту проблему нет. Здесь есть несколько тонких соображений. Мой ответ попытается дать некоторое направление.

Необходимо обработать как минимум четыре разных случая:

  1. Действительные результаты уровня приложения (соединение установлено, получен ответ, код состояния 200)
  2. Ошибки уровня приложения (установлено соединение, получен ответ, код состояния 4xx, 5xx)
  3. Ошибки сетевого ввода-вывода (соединение не установлено или ответ не получен из-за тайм-аута и т. д.)
  4. Ошибки синтаксического анализа JSON (соединение установлено, ответ получен, не удалось преобразовать JSON в объект домена модели)

Псевдокод:

  1. Завершено Future с ответом внутри которого либо ErrorResponse, либо MyResponseClass, то есть Either[ErrorResponse, MyResponseClass]:

    1. If service returns 200 status code, then parse as MyResponseClass
    2. Если служба возвращает> = 400 код состояния, то анализируйте как ErrorResponse
  2. Завершено Future с исключением внутри:

    1. Parsing Exception, or
    2. Исключение сетевого ввода-вывода (например, тайм-аут)

Future(Left(errorResponse)) против Future(throw new Exception)

Обратите внимание на разницу между Future(Left(errorResponse)) и Future(throw new Exception): мы рассматриваем только последний как неудачное будущее. Первый, несмотря на наличие Left внутри, по-прежнему считается успешно завершенным будущим.

Future.andThen против Future.recover

Обратите внимание на разницу между Future.andThen и Future.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"
)
person Mario Galic    schedule 10.11.2017

Существует 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) }
  }

это общий шаблон для сохранения результата ошибки в левой части и успеха в правой части.

person AlexITC    schedule 09.11.2017