Make a request to a remote service
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
- Play Documentation. Calling REST APIs with Play WS.
- Akka Documentation. Circuit Documentation.