Programming paradigms

As explained here ServiceTalk supports multiple programming paradigms for users to use. In this document we elaborate on how HTTP module provides support for these programming paradigms.

Whenever a payload body is aggregated into a single object (the aggregated paradigms below, or any filter that calls toRequest()/toResponse()), the whole body is buffered in memory. To guard against excessive memory consumption, ServiceTalk caps the size of an aggregated body at 4 MiB by default. Requests that exceed the server’s limit are rejected with a 413 Payload Too Large; oversized responses surface a PayloadTooLargeException to the client.

The limit is measured against the fully decoded payload buffered at the point of aggregation — that is, after any decompression or body transformation applied by filters earlier in the pipeline — not the bytes received from the network nor any declared Content-Length. A small compressed body that expands beyond the limit once decoded is therefore rejected.

Adjust or disable this via HttpServerBuilder#maxAggregatedPayloadSize(int) / SingleAddressHttpClientBuilder#maxAggregatedPayloadSize(int) (pass 0 to disable). A programmatically configured limit is definitive: it is always enforced and never runs in warn-only mode. Users who unexpectedly hit the default limit can temporarily change it globally with the io.servicetalk.http.netty.temporaryDefaultMaxAggregatedPayloadSize system property, which — unlike the builder methods — also accepts -1 for warn-only mode, where oversized aggregated messages are still served/delivered but a warning is logged (rate-limited to once every five minutes per client/server) so you can evaluate the limit before enforcing it. An explicit builder call takes precedence over the property. This is a temporary property that will be removed in future releases. Bodies consumed purely as a stream are not affected.

Server

An HTTP Server in ServiceTalk uses a user provided Service that implements all the business logic for that server. ServiceTalk allows users to write these Service implementations in different programming paradigms.

Blocking and Aggregated

This programming paradigm exposes the request (including the payload body) as a single aggregated object, and the response (including the payload body) is also a single aggregated object. The Service directly returns the response object and there are no asynchronous primitives involved.

HttpServers.forPort(8080)
  .listenBlockingAndAwait((ctx, request, responseFactory) ->
    responseFactory.ok().payloadBody("Hello World!", textSerializer()))
  .awaitShutdown();

Blocking and Streaming

This programming paradigm exposes the request payload body as an Iterable of Buffers and the response payload body is written via an HttpPayloadWriter or an HttpOutputStream. There are no asynchronous primitives involved.

HttpServers.forPort(8080).listenBlockingStreamingAndAwait((ctx, request, response) -> {
  try (HttpPayloadWriter<String> payloadWriter = response.sendMetaData(textSerializer())) {
    payloadWriter.write("Hello");
    payloadWriter.write(" World!");
  }
}).awaitShutdown();

Asynchronous and Aggregated

This programming paradigm exposes the request payload body as a single aggregated object, and the response payload body is also a single aggregated object. The Service provides the response by completing a Single which is an asynchronous primitive.

HttpServers.forPort(8080)
  .listenAndAwait((ctx, request, responseFactory) ->
    succeeded(responseFactory.ok().payloadBody("Hello World!", textSerializer())))
  .awaitShutdown();

Asynchronous and Streaming

This programming paradigm exposes the request payload body as a Publisher typically of Buffers (although other types like file regions may be added), the response meta-data is provided by completing a Single, and the response payload body is written via a Publisher.

HttpServers.forPort(8080)
  .listenStreamingAndAwait((ctx, request, responseFactory) ->
    succeeded(responseFactory.ok()
      .payloadBody(from("Hello", " World!"), textSerializer())))
  .awaitShutdown();

Client

An HTTP Client in ServiceTalk is used by users to send requests to a server. ServiceTalk allows users to create a Client for different programming paradigms.

Blocking and Aggregated

This programming paradigm expects the request (including the payload body) as a single aggregated object, and the response (including the payload body) is also a single aggregated object. The Client directly returns the response object and there are no asynchronous primitives involved.

try (BlockingHttpClient client = HttpClients.forSingleAddress("localhost", 8080).buildBlocking()) {
  HttpResponse response = client.request(client.get("/sayHello"));
  // use the response
}

Blocking and Streaming

This programming paradigm expects the request payload body as an Iterable of Buffers and the response payload body is consumed via an Iterable of Buffers. There are no asynchronous primitives involved.

try (BlockingStreamingHttpClient client = HttpClients.forSingleAddress("localhost", 8080)
      .buildBlockingStreaming()) {
  BlockingStreamingHttpResponse response = client.request(client.get("/sayHello"));
  System.out.println(response.toString((name, value) -> value));
  try (BlockingIterator<String> payload = response.payloadBody(textDeserializer()).iterator()) {
    while (payload.hasNext()) {
      System.out.println(payload.next());
    }
  }
}

Asynchronous and Aggregated

This programming paradigm expects the request payload body as a single aggregated object, and the response payload body is also a single aggregated object. The HttpClient provides the response via a Single which is an asynchronous primitive.

try (HttpClient client = HttpClients.forSingleAddress("localhost", 8080).build()) {
// This example is demonstrating asynchronous execution, but needs to prevent the main
// thread from exiting before the response has been processed. This isn't typical usage
// for a streaming API but is useful for demonstration purposes.
  CountDownLatch responseProcessedLatch = new CountDownLatch(1);
  client.request(client.get("/sayHello"))
        .doFinally(responseProcessedLatch::countDown)
        .subscribe(resp -> {
     System.out.println(resp.toString((name, value) -> value));
     System.out.println(resp.payloadBody(textDeserializer()));
  });

  responseProcessedLatch.await();
}

Asynchronous and Streaming

This programming paradigm expects the request payload body as a Publisher typically of Buffers (although other types like file regions may be added), the response meta-data is provided via a Single, and the response payload body is written via a Publisher.

try (StreamingHttpClient client = HttpClients.forSingleAddress("localhost", 8080).buildStreaming()) {
// This example is demonstrating asynchronous execution, but needs to prevent the main thread from
// exiting before the response has been processed. This isn't typical usage for a streaming API but
// is useful for demonstration purposes.
  CountDownLatch responseProcessedLatch = new CountDownLatch(1);
  client.request(client.get("/sayHello"))
        .beforeOnSuccess(response -> System.out.println(response.toString((name, value) -> value)))
        .flatMapPublisher(resp -> resp.payloadBody(textDeserializer()))
        .doFinally(responseProcessedLatch::countDown)
        .forEach(System.out::println);

  responseProcessedLatch.await();
}