jack miao
jack miao

Reputation: 1498

Best practices of handling HTTP response with scala

I have ServerA which exposes an API method for a client, which looks like this:

def methodExposed()= Action.async(json) { req =>

    val reqAsModel = request.body.extractOpt[ClientRequestModel]

    reqAsModel match {
      case Some(clientRequest) =>
        myApiService
          .doSomething(clientRequest.someList)
          .map(res => ???)
      case None =>
        Future.successful(BadRequest("could not extract request"))
    }
  }

So, I have a case class for the client request and if I cannot extract it from the request body, then I return a BadRequest with the message and otherwise I call an internal apiService to perform some action with this request.

doSomething performs an API call to ServerB that can return 3 possible responses:

  1. 200 status
  2. 400 status with body that I need to extract to a case class
  3. 500 status

doSomething looks like this:

def doSomething(list: List[String]) = {
    wSClient.url(url).withHeaders(("Content-Type", "application/json")).post(write(list)).map { response =>
      response.status match {
        case Status.BAD_REQUEST =>
          parse(response.body).extract[ServiceBResponse]
        case Status.INTERNAL_SERVER_ERROR =>
          val ex = new RuntimeException(s"ServiceB Failed with status: ${response.status} body: ${response.body}")
          throw ex
      }
    }
  }

Now I have two issues:

  1. Since the 200 returns with no body and 400 has a body, I don't know what should be the return type of doSomething
  2. How should I handle this in the controller and return the response to the client properly in methodExposed?

Upvotes: 4

Views: 7266

Answers (2)

marcospereira
marcospereira

Reputation: 12212

I would do something like this:

case class ServiceBResponse(status: Int, body: Option[String] = None)

And then, doSomething would be like:

def doSomething(list: List[String]) = {
  wSClient.url(url).withHeaders(("Content-Type", "application/json")).post(write(list)).map { response =>
    response.status match {
      case Status.OK =>
        ServiceBResponse(response.status)
      case Status.BAD_REQUEST =>
        ServiceBResponse(response.status, Option(response.body))
      case Status.INTERNAL_SERVER_ERROR =>
        val message = s"ServiceB Failed with status: ${response.status} body: ${response.body}"
        ServiceBResponse(response.status, Option(message))
    }
  }
}

Finally, inside the controller:

def methodExposed() = Action.async(json) { req =>

  val reqAsModel = request.body.extractOpt[ClientRequestModel]

  reqAsModel match {
    case Some(clientRequest) =>
      myApiService
        .doSomething(clientRequest.someList)
        .map(serviceBResponse => Status(serviceBResponse.status)(serviceBResponse.getOrElse("")))
    case None =>
      Future.successful(BadRequest("could not extract request"))
  }
}

Another alternative is directly use WSResponse:

def doSomething(list: List[String]) = {
    wSClient
        .url(url)
        .withHeaders(("Content-Type", "application/json"))
        .post(write(list))
}

And the controller:

def methodExposed() = Action.async(json) { req =>

  val reqAsModel = request.body.extractOpt[ClientRequestModel]

  reqAsModel match {
    case Some(clientRequest) =>
      myApiService
        .doSomething(clientRequest.someList)
        .map(wsResponse => Status(wsResponse.status)(wsResponse.body))
    case None =>
      Future.successful(BadRequest("could not extract request"))
  }
}

Upvotes: 1

Jacob Wang
Jacob Wang

Reputation: 4794

If 400 is a common expected error, I think the type Future[Either[Your400CaseClass, Unit]] makes sense. In terms of how methodExposed returns the result to the client depends on your business logic:

  • Is the underlying 400 something the client should be informed of? If methodExposed should return a 500 to the client when doSomething encounters a 400
  • Otherwise you can propagate the error to the client. Depending on the business logic, you may or may not want to transform your case class into another form, and potentially with a different http code.

You should throw an exception (using Future.failed) if doSomething returns 500 (or more generally, any unexpected http code).

(Lastly, I hope you're not using 500 to communicate a 'normal' error like validation / authentication error. 5xx code should only be used for exceptional and unrecoverable errors. Most http clients I know will throw an exception immediately when a 5xx is encountered, which means the user won't get the chance to handle it)

Upvotes: 0

Related Questions