Documentation

§The Play WS API

Sometimes we would like to call other HTTP services from within a Play application. Play supports this via its WS 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.

Java 1.8 uses CompletionStage to manage asynchronous code, and Java WS API relies heavily on composing CompletionStage together with different methods. If you have been using an earlier version of Play that used F.Promise, then the CompletionStage section of the migration guide will be very helpful.

§Making a Request

To use WS, first add javaWs to your build.sbt file:

libraryDependencies ++= Seq(
  javaWs
)

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.mvc.*;
import play.libs.ws.*;
import java.util.concurrent.CompletionStage;

public class Application extends Controller {

    @Inject WSClient 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.setHeader("headerKey", "headerValue")
                                        .setRequestTimeout(1000)
                                        .setQueryParameter("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<WSResponse> responsePromise = complexRequest.get();

This returns a CompletionStage<WSResponse> where the WSResponse contains the data returned from the server.

§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).setQueryParameter("paramKey", "paramValue");

§Request with additional headers

ws.url(url).setHeader("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).setHeader("Content-Type", "application/json").post(jsonString);
// OR
ws.url(url).setContentType("application/json").post(jsonString);

§Request with timeout

If you wish to specify a request timeout, you can use setRequestTimeout to set a value in milliseconds. A value of -1 can be used to set an infinite timeout.

ws.url(url).setRequestTimeout(1000).get();

§Submitting form data

To post url-form-encoded data you can set the proper header and formatted data.

ws.url(url).setContentType("application/x-www-form-urlencoded")
           .post("key1=value1&key2=value2");

§Submitting JSON data

The easiest way to post JSON data is to use the JSON library.

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);

§Submitting multipart/form data

The easiest way to post multipart/form data is to use a Source<Http.MultipartFormData.Part<Source<ByteString>, ?>, ?>

import play.mvc.Http.MultipartFormData.*;
ws.url(url).post(Source.single(new DataPart("hello", "world")));

To Upload a File you need to pass a Http.MultipartFormData.FilePart<Source<ByteString>, ?> to the Source:

Source<ByteString, ?> file = FileIO.fromFile(new File("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)));

§Streaming data

It’s also possible to stream data.

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(largeImage).execute("PUT");

The largeImage in the code snippet above is an Akka Streams 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 trait, and then adding it to the request with request.withRequestFilter(filter).

public CompletionStage<Result> index() {
    Logger logger = org.slf4j.LoggerFactory.getLogger("testLogger");
    WSRequestFilter filter = executor -> {
        WSRequestExecutor next = request -> {
            logger.debug("url = {}", request.getUrl());
            return executor.apply(request);
        };
        return next;
    };

    return ws.url(feedUrl).withRequestFilter(filter).get().thenApply(response ->
            ok("Feed title: " + response.asJson().findPath("title").asText())
    );
}

§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 response.asJson().

CompletionStage<JsonNode> jsonPromise = ws.url(url).get()
        .thenApply(WSResponse::asJson);

§Processing a response as XML

Similarly, you can process the response as XML by calling response.asXml().

CompletionStage<Document> documentPromise = ws.url(url).get()
        .thenApply(WSResponse::asXml);

§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 unwelcomed garbage collection or even out of memory errors.

WS lets you consume the response’s body incrementally by using an Akka Streams Sink. The stream() method on WSRequest returns a CompletionStage<StreamedResponse>. A StreamedResponse is a simple container holding together the response’s headers and body.

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 akka.stream.Materializer;
import akka.stream.javadsl.*;
import akka.util.ByteString;

import play.mvc.*;
import play.libs.ws.*;
import play.libs.F.Promise;

import scala.compat.java8.FutureConverters;

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<StreamedResponse> futureResponse = 
    ws.url(url).setMethod("GET").stream();

CompletionStage<Long> bytesReturned = futureResponse.thenCompose(res -> {
    Source<ByteString, ?> responseBody = res.getBody();

    // 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 = File.createTempFile("stream-to-file-", ".txt");
FileOutputStream outputStream = new FileOutputStream(file);

// Make the request
CompletionStage<StreamedResponse> futureResponse =
    ws.url(url).setMethod("GET").stream();

CompletionStage<File> downloadedFile = futureResponse.thenCompose(res -> {
    Source<ByteString, ?> responseBody = res.getBody();

    // The sink that writes to the output stream
    Sink<ByteString, CompletionStage<akka.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<StreamedResponse> futureResponse = ws.url(url).setMethod("GET").stream();

CompletionStage<Result> result = futureResponse.thenApply(response -> {
    WSResponseHeaders responseHeaders = response.getHeaders();
    Source<ByteString, ?> body = response.getBody();
    // Check that the response was successful
    if (responseHeaders.getStatus() == 200) {
        // Get the content type
        String contentType =
                Optional.ofNullable(responseHeaders.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(responseHeaders.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 on the request. Here follows another example that uses PUT instead of GET:

CompletionStage<StreamedResponse> futureResponse  =
    ws.url(url).setMethod("PUT").setBody("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();
CompletionStage<WSResponse> recoverPromise = responsePromise.handle((result, error) -> {
    if (error != null) {
        return ws.url("http://backup.example.com").get();
    } else {
        return CompletableFuture.completedFuture(result);
    }
}).thenCompose(Function.identity());

§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 CompletionStage 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.
The best way to do this is with Play’s non-blocking Timeout feature, using play.libs.concurrent.Timeout.

§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 call client.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.asynchttpclient.*;
import play.api.libs.ws.WSClientConfig;
import play.api.libs.ws.ahc.AhcWSClientConfig;
import play.api.libs.ws.ahc.AhcWSClientConfigFactory;
import play.api.libs.ws.ahc.AhcConfigBuilder;
import play.api.libs.ws.ssl.SSLConfigFactory;
import scala.concurrent.duration.Duration;

import akka.stream.Materializer;
import akka.stream.javadsl.*;
import akka.util.ByteString;
// Set up the client config (you can also use a parser here):
scala.Option<String> noneString = scala.None$.empty();
WSClientConfig wsClientConfig = new WSClientConfig(
        Duration.apply(120, TimeUnit.SECONDS), // connectionTimeout
        Duration.apply(120, TimeUnit.SECONDS), // idleTimeout
        Duration.apply(120, TimeUnit.SECONDS), // requestTimeout
        true, // followRedirects
        true, // useProxyProperties
        noneString, // userAgent
        true, // compressionEnabled / enforced
        SSLConfigFactory.defaultConfig());

AhcWSClientConfig clientConfig = AhcWSClientConfigFactory.forClientConfig(wsClientConfig);

// Add underlying asynchttpclient options to WSClient
AhcConfigBuilder builder = new AhcConfigBuilder(clientConfig);
DefaultAsyncHttpClientConfig.Builder ahcBuilder = builder.configure();
AsyncHttpClientConfig.AdditionalChannelInitializer logging = new AsyncHttpClientConfig.AdditionalChannelInitializer() {
    @Override
    public void initChannel(io.netty.channel.Channel channel) throws IOException {
        channel.pipeline().addFirst("log", new io.netty.handler.logging.LoggingHandler("debug"));
    }
};
ahcBuilder.setHttpAdditionalChannelInitializer(logging);
WSClient customWSClient = new play.libs.ws.ahc.AhcWSClient(ahcBuilder.build(), materializer);

You can also use play.libs.ws.WS.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 at all:

import akka.actor.ActorSystem;
import akka.stream.ActorMaterializer;
import akka.stream.ActorMaterializerSettings;
import org.asynchttpclient.AsyncHttpClientConfig;
import org.asynchttpclient.DefaultAsyncHttpClientConfig;
import play.libs.ws.*;
import play.libs.ws.ahc.*;

import java.util.Optional;

public class Standalone {

    public static void main(String[] args) {
        AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
                .setMaxRequestRetry(0)
                .setShutdownQuietPeriod(0)
                .setShutdownTimeout(0).build();

        String name = "wsclient";
        ActorSystem system = ActorSystem.create(name);
        ActorMaterializerSettings settings = ActorMaterializerSettings.create(system);
        ActorMaterializer materializer = ActorMaterializer.create(settings, system, name);

        WSClient client = new AhcWSClient(config, materializer);
        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);

    }
}

Once you are done with your custom client work, you must close the client:

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.

§Accessing AsyncHttpClient

You can get access to the underlying AsyncHttpClient from a WSClient.

org.asynchttpclient.AsyncHttpClient underlyingClient =
    (org.asynchttpclient.AsyncHttpClient) ws.getUnderlying();

This is important in a couple of cases. The WS library has a couple of limitations that require access to the underlying client:

§Configuring WS

Use the following properties in application.conf to configure the WS client:

§Timeouts

There are 3 different timeouts in WS. Reaching a timeout causes the WS request to interrupt.

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 AsyncClientConfig

The following advanced settings can be configured on the underlying AsyncHttpClientConfig.

Please refer to the AsyncHttpClientConfig Documentation for more information.

Note: allowPoolingConnection and allowSslConnectionPool are combined in AsyncHttpClient 2.0 into a single keepAlive variable. As such, play.ws.ning.allowPoolingConnection and play.ws.ning.allowSslConnectionPool are not valid and will throw an exception if configured.

Next: Connecting to OpenID services


Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request.