From d91b66a04ca9760219f1c6ee3a55761621bc3b03 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 27 Oct 2020 10:15:56 +0000 Subject: [PATCH 1/2] Allow Resource to add headers for range requests Closes gh-25976 --- .../reactive/resource/ResourceWebHandler.java | 6 ++-- .../resource/ResourceHttpRequestHandler.java | 8 ++--- .../ResourceHttpRequestHandlerTests.java | 31 +++++++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index 87d59e7cd7..5d33a3d449 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -352,15 +352,14 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { // Check the media type for the resource MediaType mediaType = MediaTypeFactory.getMediaType(resource).orElse(null); + setHeaders(exchange, resource, mediaType); // Content phase if (HttpMethod.HEAD.matches(exchange.getRequest().getMethodValue())) { - setHeaders(exchange, resource, mediaType); exchange.getResponse().getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes"); return Mono.empty(); } - setHeaders(exchange, resource, mediaType); ResourceHttpMessageWriter writer = getResourceHttpMessageWriter(); Assert.state(writer != null, "No ResourceHttpMessageWriter"); return writer.write(Mono.just(resource), @@ -535,6 +534,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { if (mediaType != null) { headers.setContentType(mediaType); } + if (resource instanceof HttpResource) { HttpHeaders resourceHeaders = ((HttpResource) resource).getResponseHeaders(); exchange.getResponse().getHeaders().putAll(resourceHeaders); 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 186c79d7bb..cbf5659fb4 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 @@ -508,22 +508,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator // Check the media type for the resource MediaType mediaType = getMediaType(request, resource); + setHeaders(response, resource, mediaType); // Content phase if (METHOD_HEAD.equals(request.getMethod())) { - setHeaders(response, resource, mediaType); return; } ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); if (request.getHeader(HttpHeaders.RANGE) == null) { Assert.state(this.resourceHttpMessageConverter != null, "Not initialized"); - setHeaders(response, resource, mediaType); this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage); } else { Assert.state(this.resourceRegionHttpMessageConverter != null, "Not initialized"); - response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request); try { List httpRanges = inputMessage.getHeaders().getRange(); @@ -532,7 +530,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage); } catch (IllegalArgumentException ex) { - response.setHeader("Content-Range", "bytes */" + resource.contentLength()); + response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength()); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); } } @@ -750,6 +748,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator if (mediaType != null) { response.setContentType(mediaType.toString()); } + if (resource instanceof HttpResource) { HttpHeaders resourceHeaders = ((HttpResource) resource).getResponseHeaders(); resourceHeaders.forEach((headerName, headerValues) -> { @@ -765,6 +764,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator } }); } + response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); } 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 9af18827f9..d6a9f2b7ff 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 @@ -26,6 +26,7 @@ import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -58,6 +59,7 @@ import static org.mockito.Mockito.mock; * @author Rossen Stoyanchev * @author Brian Clozel */ +@ExtendWith(GzipSupport.class) public class ResourceHttpRequestHandlerTests { private ResourceHttpRequestHandler handler; @@ -655,6 +657,35 @@ public class ResourceHttpRequestHandlerTests { assertThat(ranges[11]).isEqualTo("t."); } + @Test // gh-25976 + public void partialContentByteRangeWithEncodedResource(GzipSupport.GzippedFiles gzippedFiles) throws Exception { + String path = "js/foo.js"; + gzippedFiles.create(path); + + ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); + handler.setResourceResolvers(Arrays.asList(new EncodedResourceResolver(), new PathResourceResolver())); + handler.setLocations(Collections.singletonList(new ClassPathResource("test/", getClass()))); + handler.setServletContext(new MockServletContext()); + handler.afterPropertiesSet(); + + this.request.addHeader("Accept-Encoding", "gzip"); + this.request.addHeader("Range", "bytes=0-1"); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, path); + handler.handleRequest(this.request, this.response); + + assertThat(this.response.getStatus()).isEqualTo(206); + assertThat(this.response.getHeaderNames()).containsExactlyInAnyOrder( + "Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", + "Last-Modified", "Content-Encoding", "Vary"); + + assertThat(this.response.getContentType()).isEqualTo("text/javascript"); + assertThat(this.response.getContentLength()).isEqualTo(2); + assertThat(this.response.getHeader("Content-Range")).isEqualTo("bytes 0-1/66"); + assertThat(this.response.getHeaderValues("Accept-Ranges")).containsExactly("bytes"); + assertThat(this.response.getHeaderValues("Content-Encoding")).containsExactly("gzip"); + assertThat(this.response.getHeaderValues("Vary")).containsExactly("Accept-Encoding"); + } + @Test // SPR-14005 public void doOverwriteExistingCacheControlHeaders() throws Exception { this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); From bb4e802af5add60823672482ffa61d4c7aa4890e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 27 Oct 2020 11:14:37 +0000 Subject: [PATCH 2/2] Full header support HTTP HEAD Resource requests Allow the body to be written in order for all headers to be set as they would be on HTTP GET. The body content is ignored as a lower level. See gh-25976 --- .../web/reactive/resource/ResourceWebHandler.java | 5 ----- .../resource/ResourceWebHandlerTests.java | 4 ---- .../resource/ResourceHttpRequestHandler.java | 4 ---- .../resource/ResourceHttpRequestHandlerTests.java | 15 ++++++++++++++- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index 5d33a3d449..9da249f7c0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -355,11 +355,6 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { setHeaders(exchange, resource, mediaType); // Content phase - if (HttpMethod.HEAD.matches(exchange.getRequest().getMethodValue())) { - exchange.getResponse().getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes"); - return Mono.empty(); - } - ResourceHttpMessageWriter writer = getResourceHttpMessageWriter(); Assert.state(writer != null, "No ResourceHttpMessageWriter"); return writer.write(Mono.just(resource), 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 7bdbeff945..3b7d8204ab 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 @@ -123,10 +123,6 @@ public class ResourceWebHandlerTests { assertThat(resourceLastModifiedDate("test/foo.css") / 1000).isEqualTo(headers.getLastModified() / 1000); assertThat(headers.getFirst("Accept-Ranges")).isEqualTo("bytes"); assertThat(headers.get("Accept-Ranges").size()).isEqualTo(1); - - StepVerifier.create(exchange.getResponse().getBody()) - .expectErrorMatches(ex -> ex.getMessage().startsWith("No content was written")) - .verify(); } @Test 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 cbf5659fb4..dbd56fffa6 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 @@ -511,10 +511,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator setHeaders(response, resource, mediaType); // Content phase - if (METHOD_HEAD.equals(request.getMethod())) { - return; - } - ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); if (request.getHeader(HttpHeaders.RANGE) == null) { Assert.state(this.resourceHttpMessageConverter != null, "Not initialized"); 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 d6a9f2b7ff..9aa4454310 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 @@ -118,7 +118,6 @@ public class ResourceHttpRequestHandlerTests { assertThat(this.response.getDateHeader("Last-Modified") / 1000).isEqualTo(resourceLastModified("test/foo.css") / 1000); assertThat(this.response.getHeader("Accept-Ranges")).isEqualTo("bytes"); assertThat(this.response.getHeaders("Accept-Ranges").size()).isEqualTo(1); - assertThat(this.response.getContentAsByteArray().length).isEqualTo(0); } @Test @@ -686,6 +685,20 @@ public class ResourceHttpRequestHandlerTests { assertThat(this.response.getHeaderValues("Vary")).containsExactly("Accept-Encoding"); } + @Test // gh-25976 + public void partialContentWithHttpHead() throws Exception { + this.request.setMethod("HEAD"); + this.request.addHeader("Range", "bytes=0-1"); + 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.getContentType()).isEqualTo("text/plain"); + assertThat(this.response.getContentLength()).isEqualTo(2); + assertThat(this.response.getHeader("Content-Range")).isEqualTo("bytes 0-1/10"); + assertThat(this.response.getHeaderValues("Accept-Ranges")).containsExactly("bytes"); + } + @Test // SPR-14005 public void doOverwriteExistingCacheControlHeaders() throws Exception { this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");