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.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 {
|
|||
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 {
|
|||
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);
|
||||
|
|
|
@ -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.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
|
|||
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
|
|||
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);
|
||||
|
|
|
@ -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 {
|
|||
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);
|
||||
|
|
|
@ -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
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
|
|
|
@ -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
|
|||
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
|
|||
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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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…
Reference in New Issue