withRangeSupport

Description

Transforms the response from its inner route into a 206 Partial Content response if the client requested only part of the resource with a Range header.

Augments responses to GET requests with an Accept-Ranges: bytes header and converts them into partial responses if the request contains a valid Range request header. The requested byte-ranges are coalesced (merged) if they lie closer together than the specified rangeCoalescingThreshold argument.

In order to prevent the server from becoming overloaded with trying to prepare multipart/byteranges responses for high numbers of potentially very small ranges the directive rejects requests requesting more than rangeCountLimit ranges with a TooManyRangesRejection. Requests with unsatisfiable ranges are rejected with an UnsatisfiableRangeRejection.

The withRangeSupport() form (without parameters) uses the range-coalescing-threshold and range-count-limit settings from the akka.http.routing configuration.

This directive is transparent to non-GET requests.

See also: RFC 7233

Example

final Route route = withRangeSupport(() -> complete("ABCDEFGH"));

// test:
final String bytes348Range = ContentRange.create(RangeUnits.BYTES,
        akka.http.javadsl.model.ContentRange.create(3, 4, 8)).value();
final akka.http.javadsl.model.ContentRange bytes028Range =
        akka.http.javadsl.model.ContentRange.create(0, 2, 8);
final akka.http.javadsl.model.ContentRange bytes678Range =
        akka.http.javadsl.model.ContentRange.create(6, 7, 8);
final ActorMaterializer materializer = systemResource().materializer();

testRoute(route).run(HttpRequest.GET("/")
        .addHeader(Range.create(RangeUnits.BYTES, ByteRange.createSlice(3, 4))))
        .assertHeaderKindExists("Content-Range")
        .assertHeaderExists("Content-Range", bytes348Range)
        .assertStatusCode(StatusCodes.PARTIAL_CONTENT)
        .assertEntity("DE");

// we set "akka.http.routing.range-coalescing-threshold = 2"
// above to make sure we get two BodyParts
final TestRouteResult response = testRoute(route).run(HttpRequest.GET("/")
        .addHeader(Range.create(RangeUnits.BYTES,
                ByteRange.createSlice(0, 1), ByteRange.createSlice(1, 2), ByteRange.createSlice(6, 7))));
response.assertHeaderKindNotExists("Content-Range");

final CompletionStage<List<Multipart.ByteRanges.BodyPart>> completionStage =
        response.entity(Unmarshaller.entityToMultipartByteRanges()).getParts()
                .runFold(new ArrayList<>(), (acc, n) -> {
                    acc.add(n);
                    return acc;
                }, materializer);
try {
    final List<Multipart.ByteRanges.BodyPart> bodyParts =
            completionStage.toCompletableFuture().get(3, TimeUnit.SECONDS);
    assertEquals(2, bodyParts.toArray().length);

    final Multipart.ByteRanges.BodyPart part1 = bodyParts.get(0);
    assertEquals(bytes028Range, part1.getContentRange());
    assertEquals(ByteString.fromString("ABC"),
            part1.toStrict(1000, materializer).toCompletableFuture().get().getEntity().getData());

    final Multipart.ByteRanges.BodyPart part2 = bodyParts.get(1);
    assertEquals(bytes678Range, part2.getContentRange());
    assertEquals(ByteString.fromString("GH"),
            part2.toStrict(1000, materializer).toCompletableFuture().get().getEntity().getData());

} catch (Exception e) {
    // please handle this in production code
}
The source code for this page can be found here.