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:
@@ -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 -> {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user