parent
d8d74faab8
commit
6ee1af27c6
|
@ -36,7 +36,9 @@ import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||||
import org.springframework.http.server.reactive.HttpHandler;
|
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.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
|
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
|
||||||
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
|
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
|
||||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
|
@ -84,7 +86,8 @@ public class HttpHandlerConnector implements ClientHttpConnector {
|
||||||
mockClientRequest.setWriteHandler(requestBody -> {
|
mockClientRequest.setWriteHandler(requestBody -> {
|
||||||
log("Invoking HttpHandler for ", httpMethod, uri);
|
log("Invoking HttpHandler for ", httpMethod, uri);
|
||||||
ServerHttpRequest mockServerRequest = adaptRequest(mockClientRequest, requestBody);
|
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();
|
return Mono.empty();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -114,6 +117,11 @@ public class HttpHandlerConnector implements ClientHttpConnector {
|
||||||
return MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies).body(body);
|
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) {
|
private ClientHttpResponse adaptResponse(MockServerHttpResponse response, Flux<DataBuffer> body) {
|
||||||
HttpStatus status = Optional.ofNullable(response.getStatusCode()).orElse(HttpStatus.OK);
|
HttpStatus status = Optional.ofNullable(response.getStatusCode()).orElse(HttpStatus.OK);
|
||||||
MockClientHttpResponse clientResponse = new MockClientHttpResponse(status);
|
MockClientHttpResponse clientResponse = new MockClientHttpResponse(status);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import reactor.ipc.netty.http.server.HttpServerResponse;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.springframework.core.io.buffer.NettyDataBufferFactory;
|
import org.springframework.core.io.buffer.NettyDataBufferFactory;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,8 +55,8 @@ public class ReactorHttpHandlerAdapter
|
||||||
public Mono<Void> apply(HttpServerRequest request, HttpServerResponse response) {
|
public Mono<Void> apply(HttpServerRequest request, HttpServerResponse response) {
|
||||||
|
|
||||||
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(response.alloc());
|
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(response.alloc());
|
||||||
ReactorServerHttpRequest adaptedRequest;
|
ServerHttpRequest adaptedRequest;
|
||||||
ReactorServerHttpResponse adaptedResponse;
|
ServerHttpResponse adaptedResponse;
|
||||||
try {
|
try {
|
||||||
adaptedRequest = new ReactorServerHttpRequest(request, bufferFactory);
|
adaptedRequest = new ReactorServerHttpRequest(request, bufferFactory);
|
||||||
adaptedResponse = new ReactorServerHttpResponse(response, bufferFactory);
|
adaptedResponse = new ReactorServerHttpResponse(response, bufferFactory);
|
||||||
|
@ -66,6 +67,10 @@ public class ReactorHttpHandlerAdapter
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HttpMethod.HEAD.equals(adaptedRequest.getMethod())) {
|
||||||
|
adaptedResponse = new HttpHeadResponseDecorator(adaptedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
return this.httpHandler.handle(adaptedRequest, adaptedResponse)
|
return this.httpHandler.handle(adaptedRequest, adaptedResponse)
|
||||||
.onErrorResume(ex -> {
|
.onErrorResume(ex -> {
|
||||||
logger.error("Could not complete request", ex);
|
logger.error("Could not complete request", ex);
|
||||||
|
|
|
@ -36,6 +36,7 @@ import org.reactivestreams.Subscription;
|
||||||
|
|
||||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
@ -110,6 +111,10 @@ public class ServletHttpHandlerAdapter implements Servlet {
|
||||||
ServerHttpRequest httpRequest = createRequest(((HttpServletRequest) request), asyncContext);
|
ServerHttpRequest httpRequest = createRequest(((HttpServletRequest) request), asyncContext);
|
||||||
ServerHttpResponse httpResponse = createResponse(((HttpServletResponse) response), asyncContext);
|
ServerHttpResponse httpResponse = createResponse(((HttpServletResponse) response), asyncContext);
|
||||||
|
|
||||||
|
if (HttpMethod.HEAD.equals(httpRequest.getMethod())) {
|
||||||
|
httpResponse = new HttpHeadResponseDecorator(httpResponse);
|
||||||
|
}
|
||||||
|
|
||||||
asyncContext.addListener(ERROR_LISTENER);
|
asyncContext.addListener(ERROR_LISTENER);
|
||||||
|
|
||||||
HandlerResultSubscriber subscriber = new HandlerResultSubscriber(asyncContext);
|
HandlerResultSubscriber subscriber = new HandlerResultSubscriber(asyncContext);
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.reactivestreams.Subscription;
|
||||||
|
|
||||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -67,6 +68,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
|
||||||
ServerHttpRequest request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
|
ServerHttpRequest request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
|
||||||
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory());
|
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory());
|
||||||
|
|
||||||
|
if (HttpMethod.HEAD.equals(request.getMethod())) {
|
||||||
|
response = new HttpHeadResponseDecorator(response);
|
||||||
|
}
|
||||||
|
|
||||||
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange);
|
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange);
|
||||||
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
|
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@Override
|
||||||
protected void applyHeaders() {
|
protected void applyHeaders() {
|
||||||
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
|
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
|
||||||
|
@ -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
|
@Override
|
||||||
protected Processor<? super Publisher<? extends DataBuffer>, Void> createBodyFlushProcessor() {
|
protected Processor<? super Publisher<? extends DataBuffer>, Void> createBodyFlushProcessor() {
|
||||||
return new ResponseBodyFlushProcessor();
|
return new ResponseBodyFlushProcessor();
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import org.springframework.core.io.buffer.NettyDataBufferFactory;
|
import org.springframework.core.io.buffer.NettyDataBufferFactory;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
@ -64,8 +65,8 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBu
|
||||||
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(channel.alloc());
|
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(channel.alloc());
|
||||||
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
|
InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
|
||||||
|
|
||||||
RxNettyServerHttpRequest request;
|
ServerHttpRequest request;
|
||||||
RxNettyServerHttpResponse response;
|
ServerHttpResponse response;
|
||||||
try {
|
try {
|
||||||
request = new RxNettyServerHttpRequest(nativeRequest, bufferFactory, remoteAddress);
|
request = new RxNettyServerHttpRequest(nativeRequest, bufferFactory, remoteAddress);
|
||||||
response = new RxNettyServerHttpResponse(nativeResponse, bufferFactory);
|
response = new RxNettyServerHttpResponse(nativeResponse, bufferFactory);
|
||||||
|
@ -76,6 +77,10 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBu
|
||||||
return Observable.empty();
|
return Observable.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HttpMethod.HEAD.equals(request.getMethod())) {
|
||||||
|
response = new HttpHeadResponseDecorator(response);
|
||||||
|
}
|
||||||
|
|
||||||
Publisher<Void> result = this.httpHandler.handle(request, response)
|
Publisher<Void> result = this.httpHandler.handle(request, response)
|
||||||
.onErrorResume(ex -> {
|
.onErrorResume(ex -> {
|
||||||
logger.error("Could not complete request", ex);
|
logger.error("Could not complete request", ex);
|
||||||
|
|
|
@ -79,6 +79,15 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr
|
||||||
assertEquals(expected, performGet("/object-stream-result", MediaType.ALL, String.class).getBody());
|
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
|
@Configuration
|
||||||
@EnableWebFlux
|
@EnableWebFlux
|
||||||
|
|
|
@ -811,10 +811,10 @@ You can also use the same with request header conditions:
|
||||||
==== HTTP HEAD, OPTIONS
|
==== HTTP HEAD, OPTIONS
|
||||||
[.small]#<<web.adoc#mvc-ann-requestmapping-head-options,Same in Spring MVC>>#
|
[.small]#<<web.adoc#mvc-ann-requestmapping-head-options,Same in Spring MVC>>#
|
||||||
|
|
||||||
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, are implicitly mapped to
|
`@GetMapping` -- and also `@RequestMapping(method=HttpMethod.GET)`, support HTTP HEAD
|
||||||
and also support HTTP HEAD. An HTTP HEAD request is processed as if it were HTTP GET except
|
transparently for request mapping purposes. Controller methods don't need to change.
|
||||||
but instead of writing the body, the number of bytes are counted and the "Content-Length"
|
A response wrapper, applied in the `HttpHandler` server adapter, ensures a `"Content-Length"`
|
||||||
header set.
|
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
|
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.
|
methods listed in all `@RequestMapping` methods with matching URL patterns.
|
||||||
|
|
|
@ -852,6 +852,11 @@ instead.
|
||||||
==== HTTP HEAD and OPTIONS
|
==== HTTP HEAD and OPTIONS
|
||||||
[.small]#<<web-reactive.adoc#webflux-ann-requestmapping-head-options,Same in Spring WebFlux>>#
|
[.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
|
`@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
|
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"
|
but instead of writing the body, the number of bytes are counted and the "Content-Length"
|
||||||
|
|
Loading…
Reference in New Issue