From 662b6fe83f119fc5783ad612cfc87964bbcefb29 Mon Sep 17 00:00:00 2001 From: Gimin Kim <138752849+Gimini-3@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:25:59 +0900 Subject: [PATCH] Honor If-Range for resource range requests Ignore Range when the If-Range validator does not match the selected response representation. Apply the behavior consistently to MVC and WebFlux resource responses. Closes gh-27952 Signed-off-by: Gimin Kim <138752849+Gimini-3@users.noreply.github.com> --- .../org/springframework/http/HttpRange.java | 40 +++++++++++ .../http/codec/ResourceHttpMessageWriter.java | 2 +- .../springframework/http/HttpRangeTests.java | 68 +++++++++++++++++++ .../codec/ResourceHttpMessageWriterTests.java | 14 ++++ .../resource/ResourceWebHandlerTests.java | 18 +++++ .../DefaultEntityResponseBuilder.java | 14 ++-- ...stractMessageConverterMethodProcessor.java | 13 ++-- .../resource/ResourceHttpRequestHandler.java | 35 +++++----- .../ResourceHandlerFunctionTests.java | 44 ++++++++++++ .../HttpEntityMethodProcessorMockTests.java | 37 ++++++++++ .../ResourceHttpRequestHandlerTests.java | 29 ++++++++ 11 files changed, 287 insertions(+), 27 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpRange.java b/spring-web/src/main/java/org/springframework/http/HttpRange.java index e7052b3bf5f6..08d097a28789 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpRange.java +++ b/spring-web/src/main/java/org/springframework/http/HttpRange.java @@ -145,6 +145,46 @@ public static List parseRanges(@Nullable String ranges) { return result; } + /** + * Parse ranges from the given request headers, taking an {@code If-Range} + * header into account based on the given response headers. + *

If the request does not contain an {@code If-Range} header, or if its + * value matches the {@code ETag} or {@code Last-Modified} response header, + * this delegates to {@link #parseRanges(String)}. Otherwise, an empty list + * is returned to indicate that the {@code Range} header should be ignored. + * @param requestHeaders the request headers + * @param responseHeaders the response headers for the selected representation + * @return the parsed ranges, or an empty list if the {@code Range} header + * should be ignored + * @throws IllegalArgumentException if the range cannot be parsed + * or if the number of ranges is greater than 100 + * @since 7.1 + * @see RFC 9110, Section 13.1.5 + */ + public static List parseRanges(HttpHeaders requestHeaders, HttpHeaders responseHeaders) { + Assert.notNull(requestHeaders, "Request headers must not be null"); + Assert.notNull(responseHeaders, "Response headers must not be null"); + + List ifRangeValues = requestHeaders.get(HttpHeaders.IF_RANGE); + if (ifRangeValues != null) { + if (ifRangeValues.size() != 1 || !matchIfRange(ifRangeValues.get(0), responseHeaders)) { + return Collections.emptyList(); + } + } + return parseRanges(requestHeaders.getFirst(HttpHeaders.RANGE)); + } + + private static boolean matchIfRange(@Nullable String ifRange, HttpHeaders responseHeaders) { + if (ifRange == null) { + return false; + } + ifRange = ifRange.trim(); + if (ifRange.startsWith("\"")) { + return ifRange.equals(responseHeaders.getETag()); + } + return ifRange.equals(responseHeaders.getFirst(HttpHeaders.LAST_MODIFIED)); + } + private static HttpRange parseRange(String range) { Assert.hasLength(range, "Range String must not be empty"); int dashIdx = range.indexOf('-'); diff --git a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java index 1658ef9f9e3e..9b75ae642421 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java @@ -230,7 +230,7 @@ public Mono write(Publisher inputStream, @Nullable Res List ranges; try { - ranges = request.getHeaders().getRange(); + ranges = HttpRange.parseRanges(request.getHeaders(), response.getHeaders()); } catch (IllegalArgumentException ex) { return handleInvalidRange(response); diff --git a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java index 5e446857e612..b527dab8b162 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java @@ -105,6 +105,74 @@ void parseRanges() { assertThat(ranges.get(2).getRangeEnd(1000)).isEqualTo(999); } + @Test + void parseRangesWithMatchingIfRangeETag() { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setRange(List.of(HttpRange.createByteRange(0, 1))); + requestHeaders.set(HttpHeaders.IF_RANGE, "\"current\""); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setETag("\"current\""); + + assertThat(HttpRange.parseRanges(requestHeaders, responseHeaders)).hasSize(1); + } + + @Test + void parseRangesWithNonMatchingIfRangeETag() { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setRange(List.of(HttpRange.createByteRange(0, 1))); + requestHeaders.set(HttpHeaders.IF_RANGE, "\"stale\""); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setETag("\"current\""); + + assertThat(HttpRange.parseRanges(requestHeaders, responseHeaders)).isEmpty(); + } + + @Test + void parseRangesWithWeakIfRangeETag() { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setRange(List.of(HttpRange.createByteRange(0, 1))); + requestHeaders.set(HttpHeaders.IF_RANGE, "W/\"current\""); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setETag("W/\"current\""); + + assertThat(HttpRange.parseRanges(requestHeaders, responseHeaders)).isEmpty(); + } + + @Test + void parseRangesWithMatchingIfRangeDate() { + long lastModified = 1660000000000L; + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setRange(List.of(HttpRange.createByteRange(0, 1))); + requestHeaders.setDate(HttpHeaders.IF_RANGE, lastModified); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setLastModified(lastModified); + + assertThat(HttpRange.parseRanges(requestHeaders, responseHeaders)).hasSize(1); + } + + @Test + void parseRangesWithNonMatchingIfRangeDate() { + long lastModified = 1660000000000L; + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setRange(List.of(HttpRange.createByteRange(0, 1))); + requestHeaders.setDate(HttpHeaders.IF_RANGE, lastModified - 1000); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setLastModified(lastModified); + + assertThat(HttpRange.parseRanges(requestHeaders, responseHeaders)).isEmpty(); + } + + @Test + void invalidRangeIsIgnoredWhenIfRangeDoesNotMatch() { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set(HttpHeaders.RANGE, "invalid"); + requestHeaders.set(HttpHeaders.IF_RANGE, "\"stale\""); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setETag("\"current\""); + + assertThat(HttpRange.parseRanges(requestHeaders, responseHeaders)).isEmpty(); + } + @Test void parseRangesValidations() { diff --git a/spring-web/src/test/java/org/springframework/http/codec/ResourceHttpMessageWriterTests.java b/spring-web/src/test/java/org/springframework/http/codec/ResourceHttpMessageWriterTests.java index 1a5cf25130d0..58a980f783c4 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/ResourceHttpMessageWriterTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/ResourceHttpMessageWriterTests.java @@ -108,6 +108,20 @@ void writeSingleRegion() { StepVerifier.create(this.response.getBodyAsString()).expectNext("Spring").expectComplete().verify(); } + @Test + void ignoreRangeWhenIfRangeETagDoesNotMatch() { + this.response.getHeaders().setETag("\"current\""); + + testWrite(get("/").range(of(0, 5)).header(HttpHeaders.IF_RANGE, "\"stale\"").build()); + + assertThat(this.response.getHeaders().getFirst(HttpHeaders.CONTENT_RANGE)).isNull(); + assertThat(this.response.getHeaders().getContentLength()).isEqualTo(39L); + StepVerifier.create(this.response.getBodyAsString()) + .expectNext("Spring Framework test resource content.") + .expectComplete() + .verify(); + } + @Test void writeMultipleRegions() { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index 7aff83db6f0f..ae0fda0df3bf 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -266,6 +266,24 @@ void partialContentByteRange() { assertResponseBody(exchange, "So"); } + @Test + void ignoreRangeWhenIfRangeETagDoesNotMatch() { + this.handler.setEtagGenerator(resource -> "\"current\""); + MockServerHttpRequest request = MockServerHttpRequest.get("") + .header(HttpHeaders.RANGE, "bytes=0-1") + .header(HttpHeaders.IF_RANGE, "\"stale\"") + .build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + setPathWithinHandlerMapping(exchange, "foo.txt"); + setBestMachingPattern(exchange, "/**"); + this.handler.handle(exchange).block(TIMEOUT); + + assertThat(exchange.getResponse().getHeaders().getETag()).isEqualTo("\"current\""); + assertThat(exchange.getResponse().getHeaders().getFirst(HttpHeaders.CONTENT_RANGE)).isNull(); + assertThat(exchange.getResponse().getHeaders().getContentLength()).isEqualTo(10); + assertResponseBody(exchange, "Some text."); + } + @Test void partialContentByteRangeNoEnd() { MockServerHttpRequest request = MockServerHttpRequest.get("").header("range", "bytes=9-").build(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 0d5bd92fc84e..10f3af114ecf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -57,6 +57,7 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.SmartHttpMessageConverter; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -284,11 +285,14 @@ protected void writeEntityWithMessageConverters(Object entity, HttpServletReques if (rangeHeader != null) { Resource resource = (Resource) entity; try { - List httpRanges = HttpRange.parseRanges(rangeHeader); - serverResponse.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value()); - entity = HttpRange.toResourceRegions(httpRanges, resource); - entityClass = entity.getClass(); - entityType = RESOURCE_REGION_LIST_TYPE; + List httpRanges = HttpRange.parseRanges( + new ServletServerHttpRequest(request).getHeaders(), serverResponse.getHeaders()); + if (!httpRanges.isEmpty()) { + serverResponse.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value()); + entity = HttpRange.toResourceRegions(httpRanges, resource); + entityClass = entity.getClass(); + entityType = RESOURCE_REGION_LIST_TYPE; + } } catch (IllegalArgumentException ex) { serverResponse.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 4e8498df97f9..3033c5d9c051 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -227,11 +227,14 @@ protected void writeWithMessageConverters(@Nullable T value, MethodParameter outputMessage.getServletResponse().getStatus() == 200) { Resource resource = (Resource) value; try { - List httpRanges = inputMessage.getHeaders().getRange(); - outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value()); - body = HttpRange.toResourceRegions(httpRanges, resource); - valueType = body.getClass(); - targetType = RESOURCE_REGION_LIST_TYPE; + List httpRanges = + HttpRange.parseRanges(inputMessage.getHeaders(), outputMessage.getHeaders()); + if (!httpRanges.isEmpty()) { + outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value()); + body = HttpRange.toResourceRegions(httpRanges, resource); + valueType = body.getClass(); + targetType = RESOURCE_REGION_LIST_TYPE; + } } catch (IllegalArgumentException ex) { outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index b63dfba5e42b..876b385d8ed6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -554,31 +554,34 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon // Content phase ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); - if (request.getHeader(HttpHeaders.RANGE) == null) { - Assert.state(this.resourceHttpMessageConverter != null, "Converter not initialized"); - if (HttpMethod.HEAD.matches(request.getMethod())) { - this.resourceHttpMessageConverter.addDefaultHeaders(outputMessage, resource, mediaType); - outputMessage.flush(); - } - else { - this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage); - } - } - else { - Assert.state(this.resourceRegionHttpMessageConverter != null, "Converter not initialized"); + if (request.getHeader(HttpHeaders.RANGE) != null) { ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request); try { - List httpRanges = inputMessage.getHeaders().getRange(); - response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - this.resourceRegionHttpMessageConverter.write( - HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage); + List httpRanges = HttpRange.parseRanges(inputMessage.getHeaders(), outputMessage.getHeaders()); + if (!httpRanges.isEmpty()) { + Assert.state(this.resourceRegionHttpMessageConverter != null, "Converter not initialized"); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + this.resourceRegionHttpMessageConverter.write( + HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage); + return; + } } catch (IllegalArgumentException ex) { response.setContentType(null); response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength()); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return; } } + + Assert.state(this.resourceHttpMessageConverter != null, "Converter not initialized"); + if (HttpMethod.HEAD.matches(request.getMethod())) { + this.resourceHttpMessageConverter.addDefaultHeaders(outputMessage, resource, mediaType); + outputMessage.flush(); + } + else { + this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage); + } } protected @Nullable Resource getResource(HttpServletRequest request) throws IOException { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java index 97059add9400..d6fee6a08748 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java @@ -118,6 +118,50 @@ void getRange() throws IOException, ServletException { assertThat(servletResponse.getHeader(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); } + @Test + void ignoreRangeWhenIfRangeETagDoesNotMatch() throws IOException, ServletException { + ResourceHandlerFunction handlerFunction = + new ResourceHandlerFunction(this.resource, (resource, headers) -> headers.setETag("\"current\"")); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); + servletRequest.addHeader(HttpHeaders.RANGE, "bytes=0-5"); + servletRequest.addHeader(HttpHeaders.IF_RANGE, "\"stale\""); + ServerRequest request = new DefaultServerRequest(servletRequest, Collections.singletonList(this.messageConverter)); + + ServerResponse response = handlerFunction.handle(request); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ModelAndView mav = response.writeTo(servletRequest, servletResponse, this.context); + + assertThat(mav).isNull(); + assertThat(servletResponse.getStatus()).isEqualTo(200); + assertThat(servletResponse.getHeader(HttpHeaders.ETAG)).isEqualTo("\"current\""); + assertThat(servletResponse.getHeader(HttpHeaders.CONTENT_RANGE)).isNull(); + assertThat(servletResponse.getContentAsByteArray()).isEqualTo(Files.readAllBytes(this.resource.getFile().toPath())); + } + + @Test + void getRangeWhenIfRangeETagMatches() throws IOException, ServletException { + ResourceHandlerFunction handlerFunction = + new ResourceHandlerFunction(this.resource, (resource, headers) -> headers.setETag("\"current\"")); + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); + servletRequest.addHeader(HttpHeaders.RANGE, "bytes=0-5"); + servletRequest.addHeader(HttpHeaders.IF_RANGE, "\"current\""); + ServerRequest request = new DefaultServerRequest(servletRequest, Collections.singletonList(this.messageConverter)); + + ServerResponse response = handlerFunction.handle(request); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ModelAndView mav = response.writeTo(servletRequest, servletResponse, this.context); + + assertThat(mav).isNull(); + assertThat(servletResponse.getStatus()).isEqualTo(206); + assertThat(servletResponse.getHeader(HttpHeaders.CONTENT_RANGE)) + .isEqualTo("bytes 0-5/" + this.resource.contentLength()); + byte[] expectedBytes = new byte[6]; + try (InputStream inputStream = this.resource.getInputStream()) { + inputStream.read(expectedBytes); + } + assertThat(servletResponse.getContentAsByteArray()).isEqualTo(expectedBytes); + } + @Test void getInvalidRange() throws IOException, ServletException { MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java index 3a3c5d111d5c..a883d74e04eb 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java @@ -664,6 +664,43 @@ void shouldHandleResourceByteRange() throws Exception { assertThat(servletResponse.getStatus()).isEqualTo(206); } + @Test + void shouldIgnoreResourceByteRangeWhenIfRangeETagDoesNotMatch() throws Exception { + ByteArrayResource resource = new ByteArrayResource("Content".getBytes(StandardCharsets.UTF_8)); + ResponseEntity returnValue = ResponseEntity.ok().eTag("\"current\"").body(resource); + servletRequest.addHeader(HttpHeaders.RANGE, "bytes=0-5"); + servletRequest.addHeader(HttpHeaders.IF_RANGE, "\"stale\""); + + given(resourceMessageConverter.canWrite(ByteArrayResource.class, null)).willReturn(true); + given(resourceMessageConverter.getSupportedMediaTypes(any())).willReturn(Collections.singletonList(MediaType.ALL)); + given(resourceMessageConverter.canWrite(ByteArrayResource.class, APPLICATION_OCTET_STREAM)).willReturn(true); + + processor.handleReturnValue(returnValue, returnTypeResponseEntityResource, mavContainer, webRequest); + + then(resourceMessageConverter).should(times(1)).write( + isA(ByteArrayResource.class), eq(APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class)); + then(resourceRegionMessageConverter).should(never()).write(anyCollection(), any(), any()); + assertThat(servletResponse.getStatus()).isEqualTo(200); + assertThat(servletResponse.getHeader(HttpHeaders.CONTENT_RANGE)).isNull(); + } + + @Test + void shouldHandleResourceByteRangeWhenIfRangeETagMatches() throws Exception { + ResponseEntity returnValue = ResponseEntity.ok().eTag("\"current\"") + .body(new ByteArrayResource("Content".getBytes(StandardCharsets.UTF_8))); + servletRequest.addHeader(HttpHeaders.RANGE, "bytes=0-5"); + servletRequest.addHeader(HttpHeaders.IF_RANGE, "\"current\""); + + given(resourceRegionMessageConverter.canWrite(any(), eq(null))).willReturn(true); + given(resourceRegionMessageConverter.canWrite(any(), eq(APPLICATION_OCTET_STREAM))).willReturn(true); + + processor.handleReturnValue(returnValue, returnTypeResponseEntityResource, mavContainer, webRequest); + + then(resourceRegionMessageConverter).should(times(1)).write( + anyCollection(), eq(APPLICATION_OCTET_STREAM), any(HttpOutputMessage.class)); + assertThat(servletResponse.getStatus()).isEqualTo(206); + } + @Test void handleReturnTypeResourceIllegalByteRange() throws Exception { ResponseEntity returnValue = ResponseEntity diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index fcf135cdb448..a6bc898d9fb3 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -254,6 +254,35 @@ void partialContentByteRange() throws Exception { assertThat(this.response.getHeaders("Accept-Ranges")).hasSize(1); } + @Test + void ignoreRangeWhenIfRangeETagDoesNotMatch() throws Exception { + this.handler.setEtagGenerator(resource -> "\"current\""); + this.request.addHeader(HttpHeaders.RANGE, "bytes=0-1"); + this.request.addHeader(HttpHeaders.IF_RANGE, "\"stale\""); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); + this.handler.handleRequest(this.request, this.response); + + assertThat(this.response.getStatus()).isEqualTo(200); + assertThat(this.response.getHeader(HttpHeaders.ETAG)).isEqualTo("\"current\""); + assertThat(this.response.getHeader(HttpHeaders.CONTENT_RANGE)).isNull(); + assertThat(this.response.getContentLength()).isEqualTo(10); + assertThat(this.response.getContentAsString()).isEqualTo("Some text."); + } + + @Test + void partialContentByteRangeWhenIfRangeETagMatches() throws Exception { + this.handler.setEtagGenerator(resource -> "\"current\""); + this.request.addHeader(HttpHeaders.RANGE, "bytes=0-1"); + this.request.addHeader(HttpHeaders.IF_RANGE, "\"current\""); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.txt"); + this.handler.handleRequest(this.request, this.response); + + assertThat(this.response.getStatus()).isEqualTo(206); + assertThat(this.response.getHeader(HttpHeaders.ETAG)).isEqualTo("\"current\""); + assertThat(this.response.getHeader(HttpHeaders.CONTENT_RANGE)).isEqualTo("bytes 0-1/10"); + assertThat(this.response.getContentAsString()).isEqualTo("So"); + } + @Test void partialContentByteRangeNoEnd() throws Exception { this.request.addHeader("Range", "bytes=9-");