Browse Source

WebFlux supports HTTP HEAD

Issue: SPR-15994
pull/1595/head
Rossen Stoyanchev 7 years ago
parent
commit
6ee1af27c6
  1. 10
      spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java
  2. 75
      spring-web/src/main/java/org/springframework/http/server/reactive/HttpHeadResponseDecorator.java
  3. 9
      spring-web/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java
  4. 5
      spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java
  5. 5
      spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java
  6. 53
      spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java
  7. 9
      spring-web/src/test/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java
  8. 9
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java
  9. 8
      src/docs/asciidoc/web/webflux.adoc
  10. 5
      src/docs/asciidoc/web/webmvc.adoc

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

@ -36,7 +36,9 @@ import org.springframework.http.client.reactive.ClientHttpConnector; @@ -36,7 +36,9 @@ import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.HttpHeadResponseDecorator;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@ -84,7 +86,8 @@ public class HttpHandlerConnector implements ClientHttpConnector { @@ -84,7 +86,8 @@ public class HttpHandlerConnector implements ClientHttpConnector {
mockClientRequest.setWriteHandler(requestBody -> {
log("Invoking HttpHandler for ", httpMethod, uri);
ServerHttpRequest mockServerRequest = adaptRequest(mockClientRequest, requestBody);
this.handler.handle(mockServerRequest, mockServerResponse).subscribe(aVoid -> {}, result::onError);
ServerHttpResponse responseToUse = prepareResponse(mockServerResponse, mockServerRequest);
this.handler.handle(mockServerRequest, responseToUse).subscribe(aVoid -> {}, result::onError);
return Mono.empty();
});
@ -114,6 +117,11 @@ public class HttpHandlerConnector implements ClientHttpConnector { @@ -114,6 +117,11 @@ public class HttpHandlerConnector implements ClientHttpConnector {
return MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies).body(body);
}
private ServerHttpResponse prepareResponse(ServerHttpResponse response, ServerHttpRequest request) {
return HttpMethod.HEAD.equals(request.getMethod()) ?
new HttpHeadResponseDecorator(response) : response;
}
private ClientHttpResponse adaptResponse(MockServerHttpResponse response, Flux<DataBuffer> body) {
HttpStatus status = Optional.ofNullable(response.getStatusCode()).orElse(HttpStatus.OK);
MockClientHttpResponse clientResponse = new MockClientHttpResponse(status);

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

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.util.function.BiFunction;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
/**
* {@link ServerHttpResponse} decorator for HTTP HEAD requests.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class HttpHeadResponseDecorator extends ServerHttpResponseDecorator {
public HttpHeadResponseDecorator(ServerHttpResponse delegate) {
super(delegate);
}
/**
* Apply {@link Flux#reduce(Object, BiFunction) reduce} on the body, count
* the number of bytes produced, release data buffers without writing, and
* set the {@literal Content-Length} header.
*/
@Override
public final Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
// After Reactor Netty #171 is fixed we can return without delegating
return getDelegate().writeWith(
Flux.from(body)
.reduce(0, (current, buffer) -> {
int next = current + buffer.readableByteCount();
DataBufferUtils.release(buffer);
return next;
})
.doOnNext(count -> getHeaders().setContentLength(count))
.then(Mono.empty()));
}
/**
* Invoke {@link #setComplete()} without writing.
*
* <p>RFC 7302 allows HTTP HEAD response without content-length and it's not
* something that can be computed on a streaming response.
*/
@Override
public final Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
// Not feasible to count bytes on potentially streaming response.
// RFC 7302 allows HEAD without content-length.
return setComplete();
}
}

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

@ -27,6 +27,7 @@ import reactor.ipc.netty.http.server.HttpServerResponse; @@ -27,6 +27,7 @@ import reactor.ipc.netty.http.server.HttpServerResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
@ -54,8 +55,8 @@ public class ReactorHttpHandlerAdapter @@ -54,8 +55,8 @@ public class ReactorHttpHandlerAdapter
public Mono<Void> apply(HttpServerRequest request, HttpServerResponse response) {
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(response.alloc());
ReactorServerHttpRequest adaptedRequest;
ReactorServerHttpResponse adaptedResponse;
ServerHttpRequest adaptedRequest;
ServerHttpResponse adaptedResponse;
try {
adaptedRequest = new ReactorServerHttpRequest(request, bufferFactory);
adaptedResponse = new ReactorServerHttpResponse(response, bufferFactory);
@ -66,6 +67,10 @@ public class ReactorHttpHandlerAdapter @@ -66,6 +67,10 @@ public class ReactorHttpHandlerAdapter
return Mono.empty();
}
if (HttpMethod.HEAD.equals(adaptedRequest.getMethod())) {
adaptedResponse = new HttpHeadResponseDecorator(adaptedResponse);
}
return this.httpHandler.handle(adaptedRequest, adaptedResponse)
.onErrorResume(ex -> {
logger.error("Could not complete request", ex);

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

@ -36,6 +36,7 @@ import org.reactivestreams.Subscription; @@ -36,6 +36,7 @@ import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -110,6 +111,10 @@ public class ServletHttpHandlerAdapter implements Servlet { @@ -110,6 +111,10 @@ public class ServletHttpHandlerAdapter implements Servlet {
ServerHttpRequest httpRequest = createRequest(((HttpServletRequest) request), asyncContext);
ServerHttpResponse httpResponse = createResponse(((HttpServletResponse) response), asyncContext);
if (HttpMethod.HEAD.equals(httpRequest.getMethod())) {
httpResponse = new HttpHeadResponseDecorator(httpResponse);
}
asyncContext.addListener(ERROR_LISTENER);
HandlerResultSubscriber subscriber = new HandlerResultSubscriber(asyncContext);

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

@ -25,6 +25,7 @@ import org.reactivestreams.Subscription; @@ -25,6 +25,7 @@ import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
@ -67,6 +68,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @@ -67,6 +68,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
ServerHttpRequest request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory());
if (HttpMethod.HEAD.equals(request.getMethod())) {
response = new HttpHeadResponseDecorator(response);
}
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange);
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
}

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

@ -80,32 +80,6 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon @@ -80,32 +80,6 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon
}
}
@Override
public Mono<Void> writeWith(File file, long position, long count) {
return doCommit(() -> {
FileChannel source = null;
try {
source = FileChannel.open(file.toPath(), StandardOpenOption.READ);
StreamSinkChannel destination = getUndertowExchange().getResponseChannel();
Channels.transferBlocking(destination, source, position, count);
return Mono.empty();
}
catch (IOException ex) {
return Mono.error(ex);
}
finally {
if (source != null) {
try {
source.close();
}
catch (IOException ex) {
// ignore
}
}
}
});
}
@Override
protected void applyHeaders() {
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
@ -135,6 +109,33 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon @@ -135,6 +109,33 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon
}
}
@Override
public Mono<Void> writeWith(File file, long position, long count) {
return doCommit(() -> {
FileChannel source = null;
try {
source = FileChannel.open(file.toPath(), StandardOpenOption.READ);
StreamSinkChannel destination = getUndertowExchange().getResponseChannel();
Channels.transferBlocking(destination, source, position, count);
return Mono.empty();
}
catch (IOException ex) {
return Mono.error(ex);
}
finally {
if (source != null) {
try {
source.close();
}
catch (IOException ex) {
// ignore
}
}
}
});
}
@Override
protected Processor<? super Publisher<? extends DataBuffer>, Void> createBodyFlushProcessor() {
return new ResponseBodyFlushProcessor();

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

@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import io.netty.buffer.ByteBuf;
@ -64,8 +65,8 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBu @@ -64,8 +65,8 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBu
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(channel.alloc());
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
RxNettyServerHttpRequest request;
RxNettyServerHttpResponse response;
ServerHttpRequest request;
ServerHttpResponse response;
try {
request = new RxNettyServerHttpRequest(nativeRequest, bufferFactory, remoteAddress);
response = new RxNettyServerHttpResponse(nativeResponse, bufferFactory);
@ -76,6 +77,10 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBu @@ -76,6 +77,10 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBu
return Observable.empty();
}
if (HttpMethod.HEAD.equals(request.getMethod())) {
response = new HttpHeadResponseDecorator(response);
}
Publisher<Void> result = this.httpHandler.handle(request, response)
.onErrorResume(ex -> {
logger.error("Could not complete request", ex);

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

@ -79,6 +79,15 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr @@ -79,6 +79,15 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr
assertEquals(expected, performGet("/object-stream-result", MediaType.ALL, String.class).getBody());
}
@Test
public void httpHead() throws Exception {
String url = "http://localhost:" + this.port + "/param?name=George";
HttpHeaders headers = getRestTemplate().headForHeaders(url);
String contentType = headers.getFirst("Content-Type");
assertNotNull(contentType);
assertEquals("text/html;charset=utf-8", contentType.toLowerCase());
assertEquals(13, headers.getContentLength());
}
@Configuration
@EnableWebFlux

8
src/docs/asciidoc/web/webflux.adoc

@ -811,10 +811,10 @@ You can also use the same with request header conditions: @@ -811,10 +811,10 @@ You can also use the same with request header conditions:
==== HTTP HEAD, OPTIONS
[.small]#<<web.adoc#mvc-ann-requestmapping-head-options,Same in Spring MVC>>#
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, are implicitly mapped to
and also support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except
but instead of writing the body, the number of bytes are counted and the "Content-Length"
header set.
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, support HTTP HEAD
transparently for request mapping purposes. Controller methods don't need to change.
A response wrapper, applied in the `HttpHandler` server adapter, ensures a `"Content-Length"`
header is set to the number of bytes written and without actually writing to the response.
By default HTTP OPTIONS is handled by setting the "Allow" response header to the list of HTTP
methods listed in all `@RequestMapping` methods with matching URL patterns.

5
src/docs/asciidoc/web/webmvc.adoc

@ -852,6 +852,11 @@ instead. @@ -852,6 +852,11 @@ instead.
==== HTTP HEAD and OPTIONS
[.small]#<<web-reactive.adoc#webflux-ann-requestmapping-head-options,Same in Spring WebFlux>>#
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, support HTTP HEAD
transparently for request mapping purposes. Controller methods don't need to change.
A response wrapper, applied in `javax.servlet.http.HttpServlet`, ensures a `"Content-Length"`
header is set to the number of bytes written and without actually writing to the response.
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, are implicitly mapped to
and also support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except
but instead of writing the body, the number of bytes are counted and the "Content-Length"

Loading…
Cancel
Save