avatar

Andres Jaimes

Make a request to a remote service

By Andres Jaimes

- 4 minutes read - 672 words

This article shows how to make remote requests to services using Scala and the Play-Framework. It documents some recommended features that can improve the reliability of the request.

Setup

Make sure you add the following dependency to build.sbt

libraryDependencies += ws

GET request

GET request to a remote service

 1import scala.concurrent.Future
 2import play.api.http.Status._
 3import play.api.libs.ws.WSClient
 4import play.api.libs.json.{JsSuccess, Json}
 5
 6case class UnexpectedResponseStatus(message: String) extends Exception(message)
 7case class InvalidServiceResponse(message: String) extends Exception(message)
 8
 9class SomeClass @Inject()(
10  ws: WSClient
11) {
12  def getFromRemoteService(url: String): Future[Option[MyClass]] = {
13    ws.url(url).get map { response =>
14      response.status match {
15        case OK => response.status match {
16          response.json.validate[MyClass] match {
17            case success: JsSuccess[MyClass] => Some(success.get)
18            case _ => throw InvalidServiceResponse("Invalid response, expecting MyClass response.")
19          }
20        case BAD_REQUEST => ??? // do something
21        case NOT_FOUND => None
22        case _ => throw UnexpectedResponseStatus(s"Service response status: ${response.status}")
23      }
24    } recoverWith { case e: Exception =>
25      logger.error(s"Remote request failed: ${e.getMessage}", e)
26      Future.failed(e)
27    }
28  }
29}

POST, PUT, PATCH, DELETE requests

All other requests types can be implemented this way.

 1import scala.concurrent.Future
 2import play.api.http.Status._
 3import play.api.libs.ws.WSClient
 4import play.api.libs.json.{JsSuccess, Json}
 5
 6case class UnexpectedResponseStatus(message: String) extends Exception(message)
 7case class InvalidServiceResponse(message: String) extends Exception(message)
 8
 9class MyClass @Inject()(
10  ws: WSClient
11) {
12  def postToRemoteService(url: String): Future[MyClass] = {
13    ws.url(url).execute("POST") map { response =>
14      response.status match {
15        case OK | CREATED => response.json.validate[MyClass] match {
16          case success: JsSuccess[MyClass] => success.get
17          case _ => throw InvalidServiceResponse("Invalid response, expecting MyClass response")
18        }
19        case _ => throw UnexpectedResponseStatus(s"Service response status: ${response.status}")
20      }
21    } recoverWith { case NonFatal(e) =>
22      logger.error(s"Remote request failed: ${e.getMessage}", e)
23      Future.failed(e)
24    }
25  }
26}

The previous example shows how to make a POST request without a body. To post content you can:

1    val body = Json.obj("prop" -> myProp, "num" -> myNum)
2    // or
3    val body = Json.toJson(myPayload)
4    // and then use the post function like this:
5    ws.url(createUrl).post(body) map { response =>

Circuit-breaker

A good addition to any of the previous requests is to implement a circuit-breaker that takes care of timeouts. A circuit breaker monitors timeouts on remote calls and avoids making additional calls when they are taking too much time to respond, thus providing stability and prevent cascading failures in distributed systems. A circuit-breaker does nothing if the service is presenting connectivity issues (for example when the service is not accepting connections); we still have to use our recoverWith section to handle those cases.

 1import akka.pattern.CircuitBreaker
 2import play.api.libs.concurrent.Akka
 3import scala.concurrent.duration._
 4
 5@Singleton
 6class SomeClass @Inject()(
 7  ...
 8)(
 9  ...
10) extends Logging {
11
12  lazy val system = Akka.system // use the Play actor system. no need to spawn new threads for a simple circuit breaker.
13
14  lazy val breaker =
15    new CircuitBreaker(system.scheduler,
16      maxFailures = 5,
17      callTimeout = 5000 milliseconds,
18      resetTimeout = 10000 milliseconds).
19      onOpen(logger.error("Call to microservice API failed. Circuit breaker opened.")).
20      onClose(logger.info("Call to microservice API succeeded. Circuit breaker closed.")).
21      onHalfOpen(logger.warn("Microservice API circuit breaker half-open."))
22
23//...
24
25  def getFromRemoteService(url: String): Future[Option[MyClass]] = {
26    breaker.withCircuitBreaker(WS.url(configUrl).get) map { response =>
27      ...

Other useful additions

Other helpful options for ws are:

 1import scala.concurrent.duration._
 2
 3  ...
 4  .withRequestTimeout(1000.millis) // this line is not necessary if we have a circuit breaker
 5  .withFollowRedirects(true)
 6  .withHttpHeaders(
 7     "Charset" -> "UTF-8",
 8     "Authorization" -> s"Bearer ${someToken}",
 9     "Accept"        -> "application/json",
10     "Content-Type"  -> "application/json")
11  .post(body)
12  ...

If more than one successful response can be expected from the server, then we can check it like this:

1if (List(200, 201, 202, 204).contains(response.status)

Auth

Bearer auth

1    .withHttpHeaders(
2        ...
3        "Authorization" -> s"Bearer ${someToken}"
4        ...

Basic auth

 1import java.util.Base64
 2import java.nio.charset.StandardCharsets
 3
 4val authorization = "Basic " + Base64.getEncoder.encodeToString(
 5      (user + ":" + pwd).getBytes(StandardCharsets.UTF_8))
 6
 7...
 8    .withHttpHeaders(
 9        ...
10        "Authorization" -> authorization)
11        ...

References