§Calling REST APIs with Play WS
Sometimes we would like to call other HTTP services from within a Play application. Play supports this via its WS (“WebService”) library, which provides a way to make asynchronous HTTP calls.
There are two important parts to using the WS API: making a request, and processing the response. We’ll discuss how to make both GET and POST HTTP requests first, and then show how to process the response from the WS library. Finally, we’ll discuss some common use cases.
Note: In Play 2.6, Play WS has been split into two, with an underlying standalone client that does not depend on Play, and a wrapper on top that uses Play specific classes. In addition, shaded versions of AsyncHttpClient and Netty are now used in Play WS to minimize library conflicts, primarily so that Play’s HTTP engine can use a different version of Netty. Please see the 2.6 migration guide for more information.
§Adding WS to project
To use WS, first add javaWs
to your build.sbt
file:
libraryDependencies ++= Seq(
javaWs
)
§Enabling HTTP Caching in Play WS
Play WS supports HTTP caching, but requires a JSR-107 cache implementation to enable this feature. You can add ehcache
:
libraryDependencies += ehcache
Or you can use another JSR-107 compatible cache such as Caffeine.
Once you have the library dependencies, then enable the HTTP cache as shown on WS Cache Configuration page.
Using an HTTP cache means savings on repeated requests to backend REST services, and is especially useful when combined with resiliency features such as stale-on-error
and stale-while-revalidate
.
§Making a Request
Now any controller or component that wants to use WS will have to add the following imports and then declare a dependency on the WSClient
type to use dependency injection:
import javax.inject.Inject;
import play.libs.ws.*;
import play.mvc.*;
public class MyClient implements WSBodyReadables, WSBodyWritables {
private final WSClient ws;
@Inject
public MyClient(WSClient ws) {
this.ws = ws;
}
// ...
}
To build an HTTP request, you start with ws.url()
to specify the URL.
WSRequest request = ws.url("http://example.com");
This returns a WSRequest
that you can use to specify various HTTP options, such as setting headers. You can chain calls together to construct complex requests.
WSRequest complexRequest =
request
.addHeader("headerKey", "headerValue")
.setRequestTimeout(Duration.of(1000, ChronoUnit.MILLIS))
.addQueryParameter("paramKey", "paramValue");
You end by calling a method corresponding to the HTTP method you want to use. This ends the chain, and uses all the options defined on the built request in the WSRequest
.
CompletionStage<? extends WSResponse> responsePromise = complexRequest.get();
This returns a CompletionStage<WSResponse>
where the WSResponse
contains the data returned from the server.
Java 1.8 uses
CompletionStage
to manage asynchronous code, and Java WS API relies heavily on composingCompletionStage
together with different methods. If you have been using an earlier version of Play that usedF.Promise
, then the CompletionStage section of the migration guide will be very helpful.If you are doing any blocking work, including any kind of DNS work such as calling
java.util.URL.equals()
, then you should use a custom execution context as described in ThreadPools, preferably through aCustomExecutionContext
. You should size the pool to leave a safety margin large enough to account for failures.If you are calling out to an unreliable network, consider using
Futures.timeout
and a circuit breaker like Failsafe.
§Request with authentication
If you need to use HTTP authentication, you can specify it in the builder, using a username, password, and an WSAuthScheme
. Options for the WSAuthScheme
are BASIC
, DIGEST
, KERBEROS
, NTLM
, and SPNEGO
.
ws.url(url).setAuth("user", "password", WSAuthScheme.BASIC).get();
§Request with follow redirects
If an HTTP call results in a 302 or a 301 redirect, you can automatically follow the redirect without having to make another call.
ws.url(url).setFollowRedirects(true).get();
§Request with query parameters
You can specify query parameters for a request.
ws.url(url).addQueryParameter("paramKey", "paramValue");
§Request with additional headers
ws.url(url).addHeader("headerKey", "headerValue").get();
For example, if you are sending plain text in a particular format, you may want to define the content type explicitly.
ws.url(url).addHeader("Content-Type", "application/json").post(jsonString);
// OR
ws.url(url).setContentType("application/json").post(jsonString);
§Request with cookie
You can specify cookies for a request, using WSCookieBuilder
:
ws.url(url)
.addCookies(new WSCookieBuilder().setName("headerKey").setValue("headerValue").build())
.get();
§Request with timeout
If you wish to specify a request timeout, you can use setRequestTimeout
to set a value in milliseconds. A value of Duration.ofMillis(Long.MAX_VALUE)
can be used to set an infinite timeout.
ws.url(url).setRequestTimeout(Duration.of(1000, ChronoUnit.MILLIS)).get();
§Submitting form data
To post url-form-encoded data you can set the proper header and formatted data with a content type of “application/x-www-form-urlencoded”.
ws.url(url)
.setContentType("application/x-www-form-urlencoded")
.post("key1=value1&key2=value2");
§Submitting multipart/form data
The easiest way to post multipart/form data is to use a Source<Http.MultipartFormData.Part<Source<ByteString>, ?>, ?>
:
import play.libs.ws.ahc.AhcCurlRequestLogger;
import play.mvc.Http.MultipartFormData.*;
ws.url(url).post(Source.single(new DataPart("hello", "world")));
To upload a File as part of multipart form data, you need to pass a Http.MultipartFormData.FilePart<Source<ByteString>, ?>
to the Source
:
Source<ByteString, ?> file = FileIO.fromPath(Paths.get("hello.txt"));
FilePart<Source<ByteString, ?>> fp = new FilePart<>("hello", "hello.txt", "text/plain", file);
DataPart dp = new DataPart("key", "value");
ws.url(url).post(Source.from(Arrays.asList(fp, dp)));
§Submitting JSON data
The easiest way to post JSON data is to use Play’s JSON support, using play.libs.Json
:
import com.fasterxml.jackson.databind.JsonNode;
import play.libs.Json;
JsonNode json = Json.newObject().put("key1", "value1").put("key2", "value2");
ws.url(url).post(json);
You can also pass in a custom ObjectMapper
:
ObjectMapper objectMapper = createCustomObjectMapper();
ws.url(url).post(body(json, objectMapper));
§Submitting XML data
The easiest way to post XML data is to use Play’s XML support, using play.libs.XML
:
Document xml = play.libs.XML.fromString("<document></document>");
ws.url(url).post(xml);
§Submitting Streaming data
It’s also possible to stream data in the request body using Pekko Streams.
Here is an example showing how you could stream a large image to a different endpoint for further processing:
CompletionStage<WSResponse> wsResponse = ws.url(url).setBody(body(largeImage)).execute("PUT");
The largeImage
in the code snippet above is a Source<ByteString, ?>
.
§Request Filters
You can do additional processing on a WSRequest
by adding a request filter. A request filter is added by extending the play.libs.ws.WSRequestFilter
interface, and then adding it to the request with request.setRequestFilter(filter)
.
public CompletionStage<Result> index() {
WSRequestFilter filter =
executor ->
request -> {
logger.debug("url = " + request.getUrl());
return executor.apply(request);
};
return ws.url(feedUrl)
.setRequestFilter(filter)
.get()
.thenApply(
(WSResponse r) -> {
String title = r.getBody(json()).findPath("title").asText();
return ok("Feed title: " + title);
});
}
A sample request filter that logs the request in cURL format to SLF4J has been added in play.libs.ws.ahc.AhcCurlRequestLogger
.
ws.url("https://www.playframework.com")
.setRequestFilter(new AhcCurlRequestLogger())
.addHeader("Header-Name", "Header value")
.get();
will output:
curl \
--verbose \
--request GET \
--header 'Header-Key: Header value' \
'https://www.playframework.com'
§Processing the Response
Working with the WSResponse
is done by applying transformations such as thenApply
and thenCompose
to the CompletionStage
.
§Processing a response as JSON
You can process the response as a JsonNode
by calling r.getBody(json())
, using the default method from play.libs.ws.WSBodyReadables.json()
.
// implements WSBodyReadables or use WSBodyReadables.instance.json()
CompletionStage<JsonNode> jsonPromise = ws.url(url).get().thenApply(r -> r.getBody(json()));
§Processing a response as XML
Similarly, you can process the response as XML by calling r.getBody(xml())
, using the default method from play.libs.ws.WSBodyReadables.xml()
.
// implements WSBodyReadables or use WSBodyReadables.instance.xml()
CompletionStage<Document> documentPromise =
ws.url(url).get().thenApply(r -> r.getBody(xml()));
§Processing large responses
Calling get()
, post()
or execute()
will cause the body of the response to be loaded into memory before the response is made available. When you are downloading a large, multi-gigabyte file, this may result in unwelcome garbage collection or even out of memory errors.
You can consume the response’s body incrementally by using an Pekko Streams Sink
. The stream()
method on WSRequest
returns a CompletionStage<WSResponse>
, where the WSResponse
contains a getBodyAsStream()
method that provides a Source<ByteString, ?>
.
Note: In 2.5.x, a
StreamedResponse
was returned in response to arequest.stream()
call. In 2.6.x, a standardWSResponse
is returned, and thegetBodyAsSource()
method should be used to return the Source.
Any controller or component that wants to leverage the WS streaming functionality will have to add the following imports and dependencies:
import javax.inject.Inject;
import org.apache.pekko.stream.Materializer;
import org.apache.pekko.stream.javadsl.*;
import play.libs.ws.*;
import play.mvc.*;
public class MyController extends Controller {
@Inject WSClient ws;
@Inject Materializer materializer;
// ...
}
Here is a trivial example that uses a folding Sink
to count the number of bytes returned by the response:
// Make the request
CompletionStage<WSResponse> futureResponse = ws.url(url).setMethod("GET").stream();
CompletionStage<Long> bytesReturned =
futureResponse.thenCompose(
res -> {
Source<ByteString, ?> responseBody = res.getBodyAsSource();
// Count the number of bytes returned
Sink<ByteString, CompletionStage<Long>> bytesSum =
Sink.fold(0L, (total, bytes) -> total + bytes.length());
return responseBody.runWith(bytesSum, materializer);
});
Alternatively, you could also stream the body out to another location. For example, a file:
File file = java.nio.file.Files.createTempFile("stream-to-file-", ".txt").toFile();
OutputStream outputStream = java.nio.file.Files.newOutputStream(file.toPath());
// Make the request
CompletionStage<WSResponse> futureResponse = ws.url(url).setMethod("GET").stream();
CompletionStage<File> downloadedFile =
futureResponse.thenCompose(
res -> {
Source<ByteString, ?> responseBody = res.getBodyAsSource();
// The sink that writes to the output stream
Sink<ByteString, CompletionStage<org.apache.pekko.Done>> outputWriter =
Sink.<ByteString>foreach(bytes -> outputStream.write(bytes.toArray()));
// materialize and run the stream
CompletionStage<File> result =
responseBody
.runWith(outputWriter, materializer)
.whenComplete(
(value, error) -> {
// Close the output stream whether there was an error or not
try {
outputStream.close();
} catch (IOException e) {
}
})
.thenApply(v -> file);
return result;
});
Another common destination for response bodies is to stream them back from a controller’s Action
:
// Make the request
CompletionStage<WSResponse> futureResponse = ws.url(url).setMethod("GET").stream();
CompletionStage<Result> result =
futureResponse.thenApply(
response -> {
Source<ByteString, ?> body = response.getBodyAsSource();
// Check that the response was successful
if (response.getStatus() == 200) {
// Get the content type
String contentType =
Optional.ofNullable(response.getHeaders().get("Content-Type"))
.map(contentTypes -> contentTypes.get(0))
.orElse("application/octet-stream");
// If there's a content length, send that, otherwise return the body chunked
Optional<String> contentLength =
Optional.ofNullable(response.getHeaders().get("Content-Length"))
.map(contentLengths -> contentLengths.get(0));
if (contentLength.isPresent()) {
return ok().sendEntity(
new HttpEntity.Streamed(
body,
Optional.of(Long.parseLong(contentLength.get())),
Optional.of(contentType)));
} else {
return ok().chunked(body).as(contentType);
}
} else {
return new Result(Status.BAD_GATEWAY);
}
});
As you may have noticed, before calling stream()
we need to set the HTTP method to use by calling setMethod(String)
on the request. Here follows another example that uses PUT
instead of GET
:
CompletionStage<WSResponse> futureResponse =
ws.url(url).setMethod("PUT").setBody(body("some body")).stream();
Of course, you can use any other valid HTTP verb.
§Common Patterns and Use Cases
§Chaining WS calls
You can chain WS calls by using thenCompose
.
final CompletionStage<WSResponse> responseThreePromise =
ws.url(urlOne)
.get()
.thenCompose(responseOne -> ws.url(responseOne.getBody()).get())
.thenCompose(responseTwo -> ws.url(responseTwo.getBody()).get());
§Exception recovery
If you want to recover from an exception in the call, you can use handle
or exceptionally
to substitute a response.
CompletionStage<WSResponse> responsePromise = ws.url("http://example.com").get();
responsePromise.handle(
(result, error) -> {
if (error != null) {
return ws.url("http://backup.example.com").get();
} else {
return CompletableFuture.completedFuture(result);
}
});
§Using in a controller
You can map a CompletionStage<WSResponse>
to a CompletionStage<Result>
that can be handled directly by the Play server, using the asynchronous action pattern defined in Handling Asynchronous Results.
public CompletionStage<Result> index() {
return ws.url(feedUrl)
.get()
.thenApply(response -> ok("Feed title: " + response.asJson().findPath("title").asText()));
}
§Using WSClient with Futures Timeout
If a chain of WS calls does not complete in time, it may be useful to wrap the result in a timeout block, which will return a failed Future if the chain does not complete in time – this is more generic than using withRequestTimeout
, which only applies to a single request.
The best way to do this is with Play’s non-blocking timeout feature, using Futures.timeout
and CustomExecutionContext
to ensure some kind of resolution:
public CompletionStage<Result> index() {
CompletionStage<Result> f =
futures.timeout(
ws.url("http://playframework.com")
.get()
.thenApplyAsync(
result -> {
try {
Thread.sleep(10000L);
return Results.ok();
} catch (InterruptedException e) {
return Results.status(SERVICE_UNAVAILABLE);
}
},
customExecutionContext),
1L,
TimeUnit.SECONDS);
return f.handleAsync(
(result, e) -> {
if (e != null) {
if (e instanceof CompletionException) {
Throwable completionException = e.getCause();
if (completionException instanceof TimeoutException) {
return Results.status(SERVICE_UNAVAILABLE, "Service has timed out");
} else {
return internalServerError(e.getMessage());
}
} else {
logger.error("Unknown exception " + e.getMessage(), e);
return internalServerError(e.getMessage());
}
} else {
return result;
}
});
}
§Directly creating WSClient
We recommend that you get your WSClient
instances using dependency injection as described above. WSClient
instances created through dependency injection are simpler to use because they are automatically created when the application starts and cleaned up when the application stops.
However, if you choose, you can instantiate a WSClient
directly from code and use this for making requests or for configuring underlying AsyncHttpClient
options.
Note: If you create a
WSClient
manually then you must callclient.close()
to clean it up when you’ve finished with it. Each client creates its own thread pool. If you fail to close the client or if you create too many clients then you will run out of threads or file handles -— you’ll get errors like “Unable to create new native thread” or “too many open files” as the underlying resources are consumed.
Here is an example of how to create a WSClient
instance by yourself:
import org.apache.pekko.stream.Materializer;
import org.apache.pekko.stream.javadsl.*;
import org.apache.pekko.util.ByteString;
import play.mvc.Results;
// Set up the client config (you can also use a parser here):
// play.api.Configuration configuration = ... // injection
// play.Environment environment = ... // injection
WSClient customWSClient =
play.libs.ws.ahc.AhcWSClient.create(
play.libs.ws.ahc.AhcWSClientConfigFactory.forConfig(
configuration.underlying(), environment.classLoader()),
null, // no HTTP caching
materializer);
You can also use play.test.WSTestClient.newClient
to create an instance of WSClient
in a functional test. See JavaTestingWebServiceClients for more details.
Or, you can run the WSClient
completely standalone without involving a running Play application or configuration at all:
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.stream.Materializer;
import org.apache.pekko.stream.SystemMaterializer;
import play.shaded.ahc.org.asynchttpclient.*;
import play.libs.ws.*;
import play.libs.ws.ahc.*;
import org.junit.Test;
// Set up Pekko
String name = "wsclient";
ActorSystem system = ActorSystem.create(name);
Materializer materializer = SystemMaterializer.get(system).materializer();
// Set up AsyncHttpClient directly from config
AsyncHttpClientConfig asyncHttpClientConfig =
new DefaultAsyncHttpClientConfig.Builder()
.setMaxRequestRetry(0)
.setShutdownQuietPeriod(0)
.setShutdownTimeout(0)
.build();
AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient(asyncHttpClientConfig);
// Set up WSClient instance directly from asynchttpclient.
WSClient client = new AhcWSClient(asyncHttpClient, materializer);
// Call out to a remote system and then and close the client and pekko.
client
.url("http://www.google.com")
.get()
.whenComplete(
(r, e) -> {
Optional.ofNullable(r)
.ifPresent(
response -> {
String statusText = response.getStatusText();
System.out.println("Got a response " + statusText);
});
})
.thenRun(
() -> {
try {
client.close();
} catch (Exception e) {
e.printStackTrace();
}
})
.thenRun(system::terminate);
This can be useful in cases where there is a specific HTTP client option that isn’t accessible from config.
If you want to run WSClient
standalone, but still use configuration (including SSL), you can use a configuration parser like this:
// Set up Pekko
String name = "wsclient";
ActorSystem system = ActorSystem.create(name);
Materializer materializer = Materializer.matFromSystem(system);
// Read in config file from application.conf
Config conf = ConfigFactory.load();
WSConfigParser parser = new WSConfigParser(conf, ClassLoader.getSystemClassLoader());
AhcWSClientConfig clientConf = AhcWSClientConfigFactory.forClientConfig(parser.parse());
// Start up asynchttpclient
final DefaultAsyncHttpClientConfig asyncHttpClientConfig =
new AhcConfigBuilder(clientConf).configure().build();
final DefaultAsyncHttpClient asyncHttpClient =
new DefaultAsyncHttpClient(asyncHttpClientConfig);
// Create a new WSClient, and then close the client.
WSClient client = new AhcWSClient(asyncHttpClient, materializer);
client.close();
system.terminate();
Again, once you are done with your custom client work, you must close the client, or you will leak threads:
try {
customWSClient.close();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
Ideally, you should only close a client after you know all requests have been completed. You should not use try-with-resources
to automatically close a WSClient instance, because WSClient logic is asynchronous and try-with-resources
only supports synchronous code in its body.
§Custom BodyReadables and BodyWritables
Play WS comes with rich type support for bodies in the form of play.libs.ws.WSBodyWritables
, which contains methods for converting input such as JsonNode
or XML
in the body of a WSRequest
into a ByteString
or Source<ByteString, ?>
, and play.libs.ws.WSBodyReadables
, which contains methods that read the body of a WSResponse
from a ByteString
or Source[ByteString, _]
and return the appropriate type, such as JsValue
or XML. The default methods are available to you through the WSRequest and WSResponse, but you can also use custom types with response.getBody(myReadable())
and request.post(myWritable(data))
. This is especially useful if you want to use a custom library, i.e. you would like to stream XML through STaX API.
§Creating a Custom Readable
You can create a custom readable by parsing the response:
public interface URLBodyReadables {
default BodyReadable<java.net.URL> url() {
return response -> {
try {
String s = response.getBody();
return java.net.URI.create(s).toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
};
}
}
§Creating a Custom BodyWritable
You can create a custom body writable to a request as follows, using an InMemoryBodyWritable
. To specify a custom body writable with streaming, use a SourceBodyWritable
.
public interface URLBodyWritables {
default InMemoryBodyWritable body(java.net.URL url) {
try {
String s = url.toURI().toString();
ByteString byteString = ByteString.fromString(s);
return new InMemoryBodyWritable(byteString, "text/plain");
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
§Standalone WS
If you want to call WS outside of Play altogether, you can use the standalone version of Play WS, which does not depend on any Play libraries. You can do this by adding play-ahc-ws-standalone
to your project:
libraryDependencies += "org.playframework" %% "play-ahc-ws-standalone" % playWSStandalone
Please see https://github.com/playframework/play-ws and the 2.6 migration guide for more information.
§Accessing AsyncHttpClient
You can get access to the underlying shaded AsyncHttpClient from a WSClient
.
play.shaded.ahc.org.asynchttpclient.AsyncHttpClient underlyingClient =
(play.shaded.ahc.org.asynchttpclient.AsyncHttpClient) ws.getUnderlying();
§Configuring WS
Use the following properties in application.conf
to configure the WS client:
play.ws.followRedirects
: Configures the client to follow 301 and 302 redirects (default is true).play.ws.useProxyProperties
: To use the system http proxy settings(http.proxyHost, http.proxyPort) (default is true).play.ws.useragent
: To configure the User-Agent header field.play.ws.compressionEnabled
: Set it to true to use gzip/deflater encoding (default is false).
§Timeouts
There are 3 different timeouts in WS. Reaching a timeout causes the WS request to interrupt.
play.ws.timeout.connection
: The maximum time to wait when connecting to the remote host (default is 120 seconds).play.ws.timeout.idle
: The maximum time the request can stay idle (connection is established but waiting for more data) (default is 120 seconds).play.ws.timeout.request
: The total time you accept a request to take (it will be interrupted even if the remote host is still sending data) (default is 120 seconds).
The request timeout can be overridden for a specific connection with setTimeout()
(see “Making a Request” section).
§Configuring WS with SSL
To configure WS for use with HTTP over SSL/TLS (HTTPS), please see Configuring WS SSL.
§Configuring WS with Caching
To configure WS for use with HTTP caching, please see Configuring WS Cache.
§Configuring AsyncClientConfig
The following advanced settings can be configured on the underlying AsyncHttpClientConfig.
Please refer to the AsyncHttpClientConfig Documentation for more information.
play.ws.ahc.keepAlive
play.ws.ahc.maxConnectionsPerHost
play.ws.ahc.maxConnectionsTotal
play.ws.ahc.maxConnectionLifetime
play.ws.ahc.idleConnectionInPoolTimeout
play.ws.ahc.maxNumberOfRedirects
play.ws.ahc.maxRequestRetry
play.ws.ahc.disableUrlEncoding