Add range requests support in ResourceWebHandler

This commit handles range requests in `ResourceWebHandler`, using the
`ResourceHttpMessageWriter` configured within the handler.

This `WebHandler` will add a `HTTP_RANGE_REQUEST_HINT` writer hint
containing all the HTTP Range information of the request.

Issue: SPR-14664
This commit is contained in:
Brian Clozel 2016-09-12 15:25:47 +02:00
parent a7a9e36ca0
commit 3596e72050
3 changed files with 37 additions and 27 deletions

View File

@ -54,6 +54,7 @@ import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
import org.springframework.web.reactive.accept.PathExtensionContentTypeResolver;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
@ -82,6 +83,7 @@ import org.springframework.web.server.WebHandler;
* client.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
public class ResourceWebHandler
@ -188,7 +190,7 @@ public class ResourceWebHandler
}
/**
* Return the list of configured resource converters.
* Return the configured resource message writer.
*/
public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
return this.resourceHttpMessageWriter;
@ -333,15 +335,12 @@ public class ResourceWebHandler
return Mono.empty();
}
// TODO: range requests
setHeaders(exchange, resource, mediaType);
return this.resourceHttpMessageWriter.write(
Mono.just(resource), ResolvableType.forClass(Resource.class),
mediaType, exchange.getResponse(), Collections.emptyMap());
return this.resourceHttpMessageWriter.write(Mono.just(resource),
null, ResolvableType.forClass(Resource.class), mediaType,
exchange.getRequest(), exchange.getResponse(), Collections.emptyMap());
}
catch (IOException ex) {
catch (IOException|ResponseStatusException ex) {
return Mono.error(ex);
}
});

View File

@ -16,6 +16,10 @@
package org.springframework.web.reactive.resource;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.web.reactive.HandlerMapping.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -25,12 +29,17 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
@ -44,14 +53,12 @@ import org.springframework.util.StringUtils;
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
import static org.junit.Assert.*;
import static org.springframework.web.reactive.HandlerMapping.*;
/**
* Unit tests for {@link ResourceWebHandler}.
*
@ -69,6 +76,8 @@ public class ResourceWebHandlerTests {
private WebSessionManager sessionManager = new DefaultWebSessionManager();
private DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Before
public void setUp() throws Exception {
@ -437,7 +446,6 @@ public class ResourceWebHandlerTests {
}
@Test
@Ignore
public void partialContentByteRange() throws Exception {
this.request.addHeader("Range", "bytes=0-1");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -453,7 +461,6 @@ public class ResourceWebHandlerTests {
}
@Test
@Ignore
public void partialContentByteRangeNoEnd() throws Exception {
this.request.addHeader("Range", "bytes=9-");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -469,7 +476,6 @@ public class ResourceWebHandlerTests {
}
@Test
@Ignore
public void partialContentByteRangeLargeEnd() throws Exception {
this.request.addHeader("Range", "bytes=9-10000");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -485,7 +491,6 @@ public class ResourceWebHandlerTests {
}
@Test
@Ignore
public void partialContentSuffixRange() throws Exception {
this.request.addHeader("Range", "bytes=-1");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -501,7 +506,6 @@ public class ResourceWebHandlerTests {
}
@Test
@Ignore
public void partialContentSuffixRangeLargeSuffix() throws Exception {
this.request.addHeader("Range", "bytes=-11");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -517,20 +521,19 @@ public class ResourceWebHandlerTests {
}
@Test
@Ignore
public void partialContentInvalidRangeHeader() throws Exception {
this.request.addHeader("Range", "bytes= foo bar");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
this.handler.handle(this.exchange).blockMillis(5000);
assertEquals(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE, this.response.getStatusCode());
assertEquals("bytes */10", this.response.getHeaders().getFirst("Content-Range"));
assertEquals("bytes", this.response.getHeaders().getFirst("Accept-Ranges"));
assertEquals(1, this.response.getHeaders().get("Accept-Ranges").size());
TestSubscriber.subscribe(this.handler.handle(this.exchange))
.assertErrorWith(throwable -> {
assertThat(throwable, instanceOf(ResponseStatusException.class));
ResponseStatusException exc = (ResponseStatusException) throwable;
assertThat(exc.getStatus(), is(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE));
});
}
@Test
@Ignore
public void partialContentMultipleByteRanges() throws Exception {
this.request.addHeader("Range", "bytes=0-1, 4-5, 8-9");
this.exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt");
@ -538,11 +541,18 @@ public class ResourceWebHandlerTests {
assertEquals(HttpStatus.PARTIAL_CONTENT, this.response.getStatusCode());
assertTrue(this.response.getHeaders().getContentType().toString()
.startsWith("multipart/byteranges; boundary="));
.startsWith("multipart/byteranges;boundary="));
String boundary = "--" + this.response.getHeaders().getContentType().toString().substring(31);
String boundary = "--" + this.response.getHeaders().getContentType().toString().substring(30);
TestSubscriber.subscribe(this.response.getBody())
Mono<DataBuffer> reduced = Flux.from(this.response.getBody())
.reduce(this.bufferFactory.allocateBuffer(), (previous, current) -> {
previous.write(current);
DataBufferUtils.release(current);
return previous;
});
TestSubscriber.subscribe(reduced)
.assertValuesWith(buf -> {
String content = DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8);
String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true);