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
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
- Play Documentation. Calling REST APIs with Play WS.
- Akka Documentation. Circuit Documentation.