WebFlux supports HTTP HEAD

Issue: SPR-15994
This commit is contained in:
Rossen Stoyanchev
2017-09-27 14:57:56 -04:00
parent d8d74faab8
commit 6ee1af27c6
10 changed files with 153 additions and 35 deletions

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();