Skip to content

Commit 6ee1af2

Browse files
committed
WebFlux supports HTTP HEAD
Issue: SPR-15994
1 parent d8d74fa commit 6ee1af2

File tree

10 files changed

+153
-35
lines changed

10 files changed

+153
-35
lines changed

spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
import org.springframework.http.client.reactive.ClientHttpRequest;
3737
import org.springframework.http.client.reactive.ClientHttpResponse;
3838
import org.springframework.http.server.reactive.HttpHandler;
39+
import org.springframework.http.server.reactive.HttpHeadResponseDecorator;
3940
import org.springframework.http.server.reactive.ServerHttpRequest;
41+
import org.springframework.http.server.reactive.ServerHttpResponse;
4042
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
4143
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
4244
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@@ -84,7 +86,8 @@ public Mono<ClientHttpResponse> connect(HttpMethod httpMethod, URI uri,
8486
mockClientRequest.setWriteHandler(requestBody -> {
8587
log("Invoking HttpHandler for ", httpMethod, uri);
8688
ServerHttpRequest mockServerRequest = adaptRequest(mockClientRequest, requestBody);
87-
this.handler.handle(mockServerRequest, mockServerResponse).subscribe(aVoid -> {}, result::onError);
89+
ServerHttpResponse responseToUse = prepareResponse(mockServerResponse, mockServerRequest);
90+
this.handler.handle(mockServerRequest, responseToUse).subscribe(aVoid -> {}, result::onError);
8891
return Mono.empty();
8992
});
9093

@@ -114,6 +117,11 @@ private ServerHttpRequest adaptRequest(MockClientHttpRequest request, Publisher<
114117
return MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies).body(body);
115118
}
116119

120+
private ServerHttpResponse prepareResponse(ServerHttpResponse response, ServerHttpRequest request) {
121+
return HttpMethod.HEAD.equals(request.getMethod()) ?
122+
new HttpHeadResponseDecorator(response) : response;
123+
}
124+
117125
private ClientHttpResponse adaptResponse(MockServerHttpResponse response, Flux<DataBuffer> body) {
118126
HttpStatus status = Optional.ofNullable(response.getStatusCode()).orElse(HttpStatus.OK);
119127
MockClientHttpResponse clientResponse = new MockClientHttpResponse(status);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.http.server.reactive;
17+
18+
import java.util.function.BiFunction;
19+
20+
import org.reactivestreams.Publisher;
21+
import reactor.core.publisher.Flux;
22+
import reactor.core.publisher.Mono;
23+
24+
import org.springframework.core.io.buffer.DataBuffer;
25+
import org.springframework.core.io.buffer.DataBufferUtils;
26+
27+
/**
28+
* {@link ServerHttpResponse} decorator for HTTP HEAD requests.
29+
*
30+
* @author Rossen Stoyanchev
31+
* @since 5.0
32+
*/
33+
public class HttpHeadResponseDecorator extends ServerHttpResponseDecorator {
34+
35+
36+
public HttpHeadResponseDecorator(ServerHttpResponse delegate) {
37+
super(delegate);
38+
}
39+
40+
41+
/**
42+
* Apply {@link Flux#reduce(Object, BiFunction) reduce} on the body, count
43+
* the number of bytes produced, release data buffers without writing, and
44+
* set the {@literal Content-Length} header.
45+
*/
46+
@Override
47+
public final Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
48+
49+
// After Reactor Netty #171 is fixed we can return without delegating
50+
51+
return getDelegate().writeWith(
52+
Flux.from(body)
53+
.reduce(0, (current, buffer) -> {
54+
int next = current + buffer.readableByteCount();
55+
DataBufferUtils.release(buffer);
56+
return next;
57+
})
58+
.doOnNext(count -> getHeaders().setContentLength(count))
59+
.then(Mono.empty()));
60+
}
61+
62+
/**
63+
* Invoke {@link #setComplete()} without writing.
64+
*
65+
* <p>RFC 7302 allows HTTP HEAD response without content-length and it's not
66+
* something that can be computed on a streaming response.
67+
*/
68+
@Override
69+
public final Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
70+
// Not feasible to count bytes on potentially streaming response.
71+
// RFC 7302 allows HEAD without content-length.
72+
return setComplete();
73+
}
74+
75+
}

spring-web/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.commons.logging.Log;
2828
import org.apache.commons.logging.LogFactory;
2929
import org.springframework.core.io.buffer.NettyDataBufferFactory;
30+
import org.springframework.http.HttpMethod;
3031
import org.springframework.util.Assert;
3132

3233
/**
@@ -54,8 +55,8 @@ public ReactorHttpHandlerAdapter(HttpHandler httpHandler) {
5455
public Mono<Void> apply(HttpServerRequest request, HttpServerResponse response) {
5556

5657
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(response.alloc());
57-
ReactorServerHttpRequest adaptedRequest;
58-
ReactorServerHttpResponse adaptedResponse;
58+
ServerHttpRequest adaptedRequest;
59+
ServerHttpResponse adaptedResponse;
5960
try {
6061
adaptedRequest = new ReactorServerHttpRequest(request, bufferFactory);
6162
adaptedResponse = new ReactorServerHttpResponse(response, bufferFactory);
@@ -66,6 +67,10 @@ public Mono<Void> apply(HttpServerRequest request, HttpServerResponse response)
6667
return Mono.empty();
6768
}
6869

70+
if (HttpMethod.HEAD.equals(adaptedRequest.getMethod())) {
71+
adaptedResponse = new HttpHeadResponseDecorator(adaptedResponse);
72+
}
73+
6974
return this.httpHandler.handle(adaptedRequest, adaptedResponse)
7075
.onErrorResume(ex -> {
7176
logger.error("Could not complete request", ex);

spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import org.springframework.core.io.buffer.DataBufferFactory;
3838
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
39+
import org.springframework.http.HttpMethod;
3940
import org.springframework.lang.Nullable;
4041
import org.springframework.util.Assert;
4142

@@ -110,6 +111,10 @@ public void service(ServletRequest request, ServletResponse response) throws IOE
110111
ServerHttpRequest httpRequest = createRequest(((HttpServletRequest) request), asyncContext);
111112
ServerHttpResponse httpResponse = createResponse(((HttpServletResponse) response), asyncContext);
112113

114+
if (HttpMethod.HEAD.equals(httpRequest.getMethod())) {
115+
httpResponse = new HttpHeadResponseDecorator(httpResponse);
116+
}
117+
113118
asyncContext.addListener(ERROR_LISTENER);
114119

115120
HandlerResultSubscriber subscriber = new HandlerResultSubscriber(asyncContext);

spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.springframework.core.io.buffer.DataBufferFactory;
2727
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
28+
import org.springframework.http.HttpMethod;
2829
import org.springframework.util.Assert;
2930

3031
/**
@@ -67,6 +68,10 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
6768
ServerHttpRequest request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
6869
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory());
6970

71+
if (HttpMethod.HEAD.equals(request.getMethod())) {
72+
response = new HttpHeadResponseDecorator(response);
73+
}
74+
7075
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange);
7176
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
7277
}

spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -80,32 +80,6 @@ protected void applyStatusCode() {
8080
}
8181
}
8282

83-
@Override
84-
public Mono<Void> writeWith(File file, long position, long count) {
85-
return doCommit(() -> {
86-
FileChannel source = null;
87-
try {
88-
source = FileChannel.open(file.toPath(), StandardOpenOption.READ);
89-
StreamSinkChannel destination = getUndertowExchange().getResponseChannel();
90-
Channels.transferBlocking(destination, source, position, count);
91-
return Mono.empty();
92-
}
93-
catch (IOException ex) {
94-
return Mono.error(ex);
95-
}
96-
finally {
97-
if (source != null) {
98-
try {
99-
source.close();
100-
}
101-
catch (IOException ex) {
102-
// ignore
103-
}
104-
}
105-
}
106-
});
107-
}
108-
10983
@Override
11084
protected void applyHeaders() {
11185
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
@@ -135,6 +109,33 @@ protected void applyCookies() {
135109
}
136110
}
137111

112+
@Override
113+
public Mono<Void> writeWith(File file, long position, long count) {
114+
return doCommit(() -> {
115+
FileChannel source = null;
116+
try {
117+
source = FileChannel.open(file.toPath(), StandardOpenOption.READ);
118+
StreamSinkChannel destination = getUndertowExchange().getResponseChannel();
119+
Channels.transferBlocking(destination, source, position, count);
120+
return Mono.empty();
121+
}
122+
catch (IOException ex) {
123+
return Mono.error(ex);
124+
}
125+
finally {
126+
if (source != null) {
127+
try {
128+
source.close();
129+
}
130+
catch (IOException ex) {
131+
// ignore
132+
}
133+
}
134+
}
135+
});
136+
}
137+
138+
138139
@Override
139140
protected Processor<? super Publisher<? extends DataBuffer>, Void> createBodyFlushProcessor() {
140141
return new ResponseBodyFlushProcessor();

spring-web/src/test/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.commons.logging.LogFactory;
2424
import org.reactivestreams.Publisher;
2525
import org.springframework.core.io.buffer.NettyDataBufferFactory;
26+
import org.springframework.http.HttpMethod;
2627
import org.springframework.util.Assert;
2728

2829
import io.netty.buffer.ByteBuf;
@@ -64,8 +65,8 @@ public Observable<Void> handle(HttpServerRequest<ByteBuf> nativeRequest,
6465
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(channel.alloc());
6566
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
6667

67-
RxNettyServerHttpRequest request;
68-
RxNettyServerHttpResponse response;
68+
ServerHttpRequest request;
69+
ServerHttpResponse response;
6970
try {
7071
request = new RxNettyServerHttpRequest(nativeRequest, bufferFactory, remoteAddress);
7172
response = new RxNettyServerHttpResponse(nativeResponse, bufferFactory);
@@ -76,6 +77,10 @@ public Observable<Void> handle(HttpServerRequest<ByteBuf> nativeRequest,
7677
return Observable.empty();
7778
}
7879

80+
if (HttpMethod.HEAD.equals(request.getMethod())) {
81+
response = new HttpHeadResponseDecorator(response);
82+
}
83+
7984
Publisher<Void> result = this.httpHandler.handle(request, response)
8085
.onErrorResume(ex -> {
8186
logger.error("Could not complete request", ex);

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ public void objectStreamResultWithAllMediaType() throws Exception {
7979
assertEquals(expected, performGet("/object-stream-result", MediaType.ALL, String.class).getBody());
8080
}
8181

82+
@Test
83+
public void httpHead() throws Exception {
84+
String url = "http://localhost:" + this.port + "/param?name=George";
85+
HttpHeaders headers = getRestTemplate().headForHeaders(url);
86+
String contentType = headers.getFirst("Content-Type");
87+
assertNotNull(contentType);
88+
assertEquals("text/html;charset=utf-8", contentType.toLowerCase());
89+
assertEquals(13, headers.getContentLength());
90+
}
8291

8392
@Configuration
8493
@EnableWebFlux

src/docs/asciidoc/web/webflux.adoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -811,10 +811,10 @@ You can also use the same with request header conditions:
811811
==== HTTP HEAD, OPTIONS
812812
[.small]#<<web.adoc#mvc-ann-requestmapping-head-options,Same in Spring MVC>>#
813813

814-
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, are implicitly mapped to
815-
and also support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except
816-
but instead of writing the body, the number of bytes are counted and the "Content-Length"
817-
header set.
814+
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, support HTTP HEAD
815+
transparently for request mapping purposes. Controller methods don't need to change.
816+
A response wrapper, applied in the `HttpHandler` server adapter, ensures a `"Content-Length"`
817+
header is set to the number of bytes written and without actually writing to the response.
818818

819819
By default HTTP OPTIONS is handled by setting the "Allow" response header to the list of HTTP
820820
methods listed in all `@RequestMapping` methods with matching URL patterns.

src/docs/asciidoc/web/webmvc.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,11 @@ instead.
852852
==== HTTP HEAD and OPTIONS
853853
[.small]#<<web-reactive.adoc#webflux-ann-requestmapping-head-options,Same in Spring WebFlux>>#
854854

855+
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, support HTTP HEAD
856+
transparently for request mapping purposes. Controller methods don't need to change.
857+
A response wrapper, applied in `javax.servlet.http.HttpServlet`, ensures a `"Content-Length"`
858+
header is set to the number of bytes written and without actually writing to the response.
859+
855860
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, are implicitly mapped to
856861
and also support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except
857862
but instead of writing the body, the number of bytes are counted and the "Content-Length"

0 commit comments

Comments
 (0)