Make a request to a remote service

By Andres Jaimes

- 3 minutes read - 589 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

import scala.concurrent.Future
import play.api.http.Status._
import play.api.libs.ws.WSClient
import play.api.libs.json.{JsSuccess, Json}

case class UnexpectedResponseStatus(message: String) extends Exception(message)
case class InvalidServiceResponse(message: String) extends Exception(message)

class SomeClass @Inject()(
  ws: WSClient
) {
  def getFromRemoteService(url: String): Future[Option[MyClass]] = {
    ws.url(url).get map { response =>
      response.status match {
        case OK => response.status match {
          response.json.validate[MyClass] match {
            case success: JsSuccess[MyClass] => Some(success.get)
            case _ => throw InvalidServiceResponse("Invalid response, expecting MyClass response.")
          }
        case BAD_REQUEST => ??? // do something
        case NOT_FOUND => None
        case _ => throw UnexpectedResponseStatus(s"Service response status: ${response.status}")
      }
    } recoverWith { case e: Exception =>
      logger.error(s"Remote request failed: ${e.getMessage}", e)
      Future.failed(e)
    }
  }
}

POST, PUT, PATCH, DELETE requests

All other requests types can be implemented this way.

import scala.concurrent.Future
import play.api.http.Status._
import play.api.libs.ws.WSClient
import play.api.libs.json.{JsSuccess, Json}

case class UnexpectedResponseStatus(message: String) extends Exception(message)
case class InvalidServiceResponse(message: String) extends Exception(message)

class MyClass @Inject()(
  ws: WSClient
) {
  def postToRemoteService(url: String): Future[MyClass] = {
    ws.url(url).execute("POST") map { response =>
      response.status match {
        case OK | CREATED => response.json.validate[MyClass] match {
          case success: JsSuccess[MyClass] => success.get
          case _ => throw InvalidServiceResponse("Invalid response, expecting MyClass response")
        }
        case _ => throw UnexpectedResponseStatus(s"Service response status: ${response.status}")
      }
    } recoverWith { case NonFatal(e) =>
      logger.error(s"Remote request failed: ${e.getMessage}", e)
      Future.failed(e)
    }
  }
}

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

    val body = Json.obj("prop" -> myProp, "num" -> myNum)
    // or
    val body = Json.toJson(myPayload)
    // and then use the post function like this:
    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.

import akka.pattern.CircuitBreaker
import play.api.libs.concurrent.Akka
import scala.concurrent.duration._

@Singleton
class SomeClass @Inject()(
  ...
)(
  ...
) extends Logging {

  lazy val system = Akka.system // use the Play actor system. no need to spawn new threads for a simple circuit breaker.

  lazy val breaker =
    new CircuitBreaker(system.scheduler,
      maxFailures = 5,
      callTimeout = 5000 milliseconds,
      resetTimeout = 10000 milliseconds).
      onOpen(logger.error("Call to microservice API failed. Circuit breaker opened.")).
      onClose(logger.info("Call to microservice API succeeded. Circuit breaker closed.")).
      onHalfOpen(logger.warn("Microservice API circuit breaker half-open."))

//...

  def getFromRemoteService(url: String): Future[Option[MyClass]] = {
    breaker.withCircuitBreaker(WS.url(configUrl).get) map { response =>
      ...

Other useful additions

Other helpful options for ws are:

import scala.concurrent.duration._

  ...
  .withRequestTimeout(1000.millis) // this line is not necessary if we have a circuit breaker
  .withFollowRedirects(true)
  .withHttpHeaders(
     "Charset" -> "UTF-8",
     "Authorization" -> s"Bearer ${someToken}",
     "Accept"        -> "application/json",
     "Content-Type"  -> "application/json")
  .post(body)
  ...

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

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

Auth

Bearer auth

    .withHttpHeaders(
        ...
        "Authorization" -> s"Bearer ${someToken}"
        ...

Basic auth

import java.util.Base64
import java.nio.charset.StandardCharsets

val authorization = "Basic " + Base64.getEncoder.encodeToString(
      (user + ":" + pwd).getBytes(StandardCharsets.UTF_8))

...
    .withHttpHeaders(
        ...
        "Authorization" -> authorization)
        ...

References