Add HEAD support in MVC/WebFlux Resource handling

This commit introduces explicit HEAD support in Spring
MVC's ResourceHttpRequestHandler and WebFlux's ResourceWebHandler,
adding just headers but no body.

Closes gh-28291
This commit is contained in:
Arjen Poutsma
2022-04-07 13:51:21 +02:00
parent df5e3f79e1
commit 9adfa5e8b0
6 changed files with 54 additions and 19 deletions

View File

@@ -116,8 +116,27 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
private Mono<Void> writeResource(Resource resource, ResolvableType type, @Nullable MediaType mediaType,
ReactiveHttpOutputMessage message, Map<String, Object> hints) {
addHeaders(message, resource, mediaType, hints);
return zeroCopy(resource, null, message, hints)
.orElseGet(() -> {
Mono<Resource> input = Mono.just(resource);
DataBufferFactory factory = message.bufferFactory();
Flux<DataBuffer> body = this.encoder.encode(input, factory, type, message.getHeaders().getContentType(), hints);
if (logger.isDebugEnabled()) {
body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger));
}
return message.writeWith(body);
});
}
/**
* Adds the default headers for the given resource to the given message.
* @since 6.0
*/
public void addHeaders(ReactiveHttpOutputMessage message, Resource resource, @Nullable MediaType contentType, Map<String, Object> hints) {
HttpHeaders headers = message.getHeaders();
MediaType resourceMediaType = getResourceMediaType(mediaType, resource, hints);
MediaType resourceMediaType = getResourceMediaType(contentType, resource, hints);
headers.setContentType(resourceMediaType);
if (headers.getContentLength() < 0) {
@@ -126,17 +145,7 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
headers.setContentLength(length);
}
}
return zeroCopy(resource, null, message, hints)
.orElseGet(() -> {
Mono<Resource> input = Mono.just(resource);
DataBufferFactory factory = message.bufferFactory();
Flux<DataBuffer> body = this.encoder.encode(input, factory, type, resourceMediaType, hints);
if (logger.isDebugEnabled()) {
body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger));
}
return message.writeWith(body);
});
headers.set(HttpHeaders.ACCEPT_RANGES, "bytes");
}
private static MediaType getResourceMediaType(

View File

@@ -122,6 +122,14 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
return (contentLength < 0 ? null : contentLength);
}
/**
* Adds the default headers for the given resource to the given message.
* @since 6.0
*/
public void addDefaultHeaders(HttpOutputMessage message, Resource resource, @Nullable MediaType contentType) throws IOException {
addDefaultHeaders(message.getHeaders(), resource, contentType);
}
@Override
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

View File

@@ -438,10 +438,17 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
// Content phase
ResourceHttpMessageWriter writer = getResourceHttpMessageWriter();
Assert.state(writer != null, "No ResourceHttpMessageWriter");
return writer.write(Mono.just(resource),
null, ResolvableType.forClass(Resource.class), mediaType,
exchange.getRequest(), exchange.getResponse(),
Hints.from(Hints.LOG_PREFIX_HINT, exchange.getLogPrefix()));
if (HttpMethod.HEAD == httpMethod) {
writer.addHeaders(exchange.getResponse(), resource, mediaType,
Hints.from(Hints.LOG_PREFIX_HINT, exchange.getLogPrefix()));
return exchange.getResponse().setComplete();
}
else {
return writer.write(Mono.just(resource),
null, ResolvableType.forClass(Resource.class), mediaType,
exchange.getRequest(), exchange.getResponse(),
Hints.from(Hints.LOG_PREFIX_HINT, exchange.getLogPrefix()));
}
}
catch (IOException ex) {
return Mono.error(ex);

View File

@@ -120,7 +120,10 @@ public class ResourceWebHandlerTests {
assertThat(headers.containsKey("Last-Modified")).isTrue();
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);
assertThat(headers.get("Accept-Ranges")).hasSize(1);
StepVerifier.create(exchange.getResponse().getBody())
.verifyComplete();
}
@Test

View File

@@ -585,7 +585,14 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (request.getHeader(HttpHeaders.RANGE) == null) {
Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
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, "Not initialized");

View File

@@ -118,7 +118,8 @@ public class ResourceHttpRequestHandlerTests {
assertThat(this.response.containsHeader("Last-Modified")).isTrue();
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.getHeaders("Accept-Ranges")).hasSize(1);
assertThat(this.response.getContentAsByteArray()).isEmpty();
}
@Test