Support HTTP range requests in MVC Controllers

Prior to this commit, HTTP Range requests were only supported by the
ResourceHttpRequestHandler when serving static resources.

This commit improves the `HttpEntityMethodProcessor` and
the `RequestResponseBodyMethodProcessor`. They now extract
`ResourceRegion`s from the `Resource` instance returned by the
Controller and let the Resource-related message converters
handle the writing of the resource (including partial writes).

Controller methods can now handle Range requests for
return types that extend Resource or HttpEntity:

    @RequestMapping("/example/video.mp4")
    public Resource handler() { }

    @RequestMapping("/example/video.mp4")
    public HttpEntity<Resource> handler() { }

Issue: SPR-15789, SPR-13834
This commit is contained in:
Brian Clozel
2017-08-24 19:35:16 +02:00
parent d20b3cf86d
commit 582014e944
7 changed files with 118 additions and 17 deletions

View File

@@ -175,7 +175,7 @@ public class WebMvcConfigurationSupportTests {
ApplicationContext context = initContext(WebConfig.class);
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
List<HttpMessageConverter<?>> converters = adapter.getMessageConverters();
assertEquals(11, converters.size());
assertEquals(12, converters.size());
converters.stream()
.filter(converter -> converter instanceof AbstractJackson2HttpMessageConverter)
.forEach(converter -> {

View File

@@ -20,10 +20,9 @@ import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
@@ -82,6 +81,8 @@ public class HttpEntityMethodProcessorMockTests {
private HttpMessageConverter<Resource> resourceMessageConverter;
private HttpMessageConverter<Object> resourceRegionMessageConverter;
private MethodParameter paramHttpEntity;
private MethodParameter paramRequestEntity;
@@ -119,12 +120,11 @@ public class HttpEntityMethodProcessorMockTests {
given(stringHttpMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
resourceMessageConverter = mock(HttpMessageConverter.class);
given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(stringHttpMessageConverter);
converters.add(resourceMessageConverter);
processor = new HttpEntityMethodProcessor(converters);
reset(stringHttpMessageConverter);
reset(resourceMessageConverter);
resourceRegionMessageConverter = mock(HttpMessageConverter.class);
given(resourceRegionMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
processor = new HttpEntityMethodProcessor(
Arrays.asList(stringHttpMessageConverter, resourceMessageConverter, resourceRegionMessageConverter));
Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class,
Integer.TYPE, RequestEntity.class);
@@ -497,6 +497,39 @@ public class HttpEntityMethodProcessorMockTests {
assertEquals(200, servletResponse.getStatus());
}
@Test
public void shouldHandleResourceByteRange() throws Exception {
ResponseEntity<Resource> returnValue = ResponseEntity
.ok(new ByteArrayResource("Content".getBytes(StandardCharsets.UTF_8)));
servletRequest.addHeader("Range", "bytes=0-5");
given(resourceRegionMessageConverter.canWrite(any(), eq(null))).willReturn(true);
given(resourceRegionMessageConverter.canWrite(any(), eq(MediaType.APPLICATION_OCTET_STREAM))).willReturn(true);
processor.handleReturnValue(returnValue, returnTypeResponseEntityResource, mavContainer, webRequest);
then(resourceRegionMessageConverter).should(times(1)).write(
anyCollection(), eq(MediaType.APPLICATION_OCTET_STREAM),
argThat(outputMessage -> outputMessage.getHeaders().getFirst(HttpHeaders.ACCEPT_RANGES) == "bytes"));
assertEquals(206, servletResponse.getStatus());
}
@Test
public void handleReturnTypeResourceIllegalByteRange() throws Exception {
ResponseEntity<Resource> returnValue = ResponseEntity
.ok(new ByteArrayResource("Content".getBytes(StandardCharsets.UTF_8)));
servletRequest.addHeader("Range", "illegal");
given(resourceRegionMessageConverter.canWrite(any(), eq(null))).willReturn(true);
given(resourceRegionMessageConverter.canWrite(any(), eq(MediaType.APPLICATION_OCTET_STREAM))).willReturn(true);
processor.handleReturnValue(returnValue, returnTypeResponseEntityResource, mavContainer, webRequest);
then(resourceRegionMessageConverter).should(never()).write(
anyCollection(), eq(MediaType.APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class));
assertEquals(416, servletResponse.getStatus());
}
@Test //SPR-14767
public void shouldHandleValidatorHeadersInPutResponses() throws Exception {
servletRequest.setMethod("PUT");

View File

@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@@ -31,6 +32,7 @@ import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
@@ -71,6 +73,8 @@ public class RequestResponseBodyMethodProcessorMockTests {
private HttpMessageConverter<Resource> resourceMessageConverter;
private HttpMessageConverter<Object> resourceRegionMessageConverter;
private RequestResponseBodyMethodProcessor processor;
private ModelAndViewContainer mavContainer;
@@ -97,11 +101,13 @@ public class RequestResponseBodyMethodProcessorMockTests {
public void setup() throws Exception {
stringMessageConverter = mock(HttpMessageConverter.class);
given(stringMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
resourceMessageConverter = mock(HttpMessageConverter.class);
given(resourceMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
resourceRegionMessageConverter = mock(HttpMessageConverter.class);
given(resourceRegionMessageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.ALL));
processor = new RequestResponseBodyMethodProcessor(Arrays.asList(stringMessageConverter, resourceMessageConverter));
processor = new RequestResponseBodyMethodProcessor(
Arrays.asList(stringMessageConverter, resourceMessageConverter, resourceRegionMessageConverter));
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest();
@@ -364,6 +370,37 @@ public class RequestResponseBodyMethodProcessorMockTests {
verify(stringMessageConverter).write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
}
@Test
public void handleReturnTypeResourceByteRange() throws Exception {
Resource returnValue = new ByteArrayResource("Content".getBytes(StandardCharsets.UTF_8));
servletRequest.addHeader("Range", "bytes=0-5");
given(resourceRegionMessageConverter.canWrite(any(), eq(null))).willReturn(true);
given(resourceRegionMessageConverter.canWrite(any(), eq(MediaType.APPLICATION_OCTET_STREAM))).willReturn(true);
processor.handleReturnValue(returnValue, returnTypeResource, mavContainer, webRequest);
then(resourceRegionMessageConverter).should(times(1)).write(
anyCollection(), eq(MediaType.APPLICATION_OCTET_STREAM),
argThat(outputMessage -> outputMessage.getHeaders().getFirst(HttpHeaders.ACCEPT_RANGES) == "bytes"));
assertEquals(206, servletResponse.getStatus());
}
@Test
public void handleReturnTypeResourceIllegalByteRange() throws Exception {
Resource returnValue = new ByteArrayResource("Content".getBytes(StandardCharsets.UTF_8));
servletRequest.addHeader("Range", "illegal");
given(resourceRegionMessageConverter.canWrite(any(), eq(null))).willReturn(true);
given(resourceRegionMessageConverter.canWrite(any(), eq(MediaType.APPLICATION_OCTET_STREAM))).willReturn(true);
processor.handleReturnValue(returnValue, returnTypeResource, mavContainer, webRequest);
then(resourceRegionMessageConverter).should(never()).write(
anyCollection(), eq(MediaType.APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class));
assertEquals(416, servletResponse.getStatus());
}
@SuppressWarnings("unused")
@ResponseBody