Motivation:
It is generally useful to have origin http servers respond to "expect: continue-100" as soon as possible but applications without a HttpObjectAggregator in their pipelines must use boiler plate to do so. Modifications: Introduce the HttpServerExpectContinueHandler handler to make it easier. Result: Less boiler plate for http application authors.
This commit is contained in:
parent
ffd6911586
commit
b1cb059540
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2017 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you 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 io.netty.handler.codec.http;
|
||||
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
|
||||
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
|
||||
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
||||
|
||||
/**
|
||||
* Sends a <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3">100 CONTINUE</a>
|
||||
* {@link HttpResponse} to {@link HttpRequest}s which contain a 'expect: 100-continue' header. It
|
||||
* should only be used for applications which do <b>not</b> install the {@link HttpObjectAggregator}.
|
||||
* <p>
|
||||
* By default it accepts all expectations.
|
||||
* <p>
|
||||
* Since {@link HttpServerExpectContinueHandler} expects {@link HttpRequest}s it should be added after {@link
|
||||
* HttpServerCodec} but before any other handlers that might send a {@link HttpResponse}. <blockquote>
|
||||
* <pre>
|
||||
* {@link io.netty.channel.ChannelPipeline} p = ...;
|
||||
* ...
|
||||
* p.addLast("serverCodec", new {@link HttpServerCodec}());
|
||||
* p.addLast("respondExpectContinue", <b>new {@link HttpServerExpectContinueHandler}()</b>);
|
||||
* ...
|
||||
* p.addLast("handler", new HttpRequestHandler());
|
||||
* </pre>
|
||||
* </blockquote>
|
||||
*/
|
||||
public class HttpServerExpectContinueHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final FullHttpResponse EXPECTATION_FAILED = new DefaultFullHttpResponse(
|
||||
HTTP_1_1, HttpResponseStatus.EXPECTATION_FAILED, Unpooled.EMPTY_BUFFER);
|
||||
|
||||
private static final FullHttpResponse ACCEPT = new DefaultFullHttpResponse(
|
||||
HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER);
|
||||
|
||||
static {
|
||||
EXPECTATION_FAILED.headers().set(CONTENT_LENGTH, 0);
|
||||
ACCEPT.headers().set(CONTENT_LENGTH, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a {@link HttpResponse} for {@link HttpRequest}s which define an expectation. Returns {@code null} if the
|
||||
* request should be rejected. See {@link #rejectResponse(HttpRequest)}.
|
||||
*/
|
||||
protected HttpResponse acceptMessage(@SuppressWarnings("unused") HttpRequest request) {
|
||||
return ACCEPT.retainedDuplicate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate 4XX {@link HttpResponse} for the given {@link HttpRequest}.
|
||||
*/
|
||||
protected HttpResponse rejectResponse(@SuppressWarnings("unused") HttpRequest request) {
|
||||
return EXPECTATION_FAILED.retainedDuplicate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest req = (HttpRequest) msg;
|
||||
|
||||
if (HttpUtil.is100ContinueExpected(req)) {
|
||||
HttpResponse accept = acceptMessage(req);
|
||||
|
||||
if (accept == null) {
|
||||
// the expectation failed so we refuse the request.
|
||||
HttpResponse rejection = rejectResponse(req);
|
||||
ReferenceCountUtil.release(msg);
|
||||
ctx.writeAndFlush(rejection).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.writeAndFlush(accept).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
|
||||
req.headers().remove(HttpHeaderNames.EXPECT);
|
||||
}
|
||||
}
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2017 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you 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 io.netty.handler.codec.http;
|
||||
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class HttpServerExpectContinueHandlerTest {
|
||||
|
||||
@Test
|
||||
public void shouldRespondToExpectedHeader() {
|
||||
EmbeddedChannel channel = new EmbeddedChannel(new HttpServerExpectContinueHandler() {
|
||||
@Override
|
||||
protected HttpResponse acceptMessage(HttpRequest request) {
|
||||
HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
|
||||
response.headers().set("foo", "bar");
|
||||
return response;
|
||||
}
|
||||
});
|
||||
HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
|
||||
HttpUtil.set100ContinueExpected(request, true);
|
||||
|
||||
channel.writeInbound(request);
|
||||
HttpResponse response = channel.readOutbound();
|
||||
|
||||
assertThat(response.status(), is(HttpResponseStatus.CONTINUE));
|
||||
assertThat(response.headers().get("foo"), is("bar"));
|
||||
ReferenceCountUtil.release(response);
|
||||
|
||||
HttpRequest processedRequest = channel.readInbound();
|
||||
assertFalse(processedRequest.headers().contains(HttpHeaderNames.EXPECT));
|
||||
ReferenceCountUtil.release(processedRequest);
|
||||
assertFalse(channel.finishAndReleaseAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowCustomResponses() {
|
||||
EmbeddedChannel channel = new EmbeddedChannel(
|
||||
new HttpServerExpectContinueHandler() {
|
||||
@Override
|
||||
protected HttpResponse acceptMessage(HttpRequest request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse rejectResponse(HttpRequest request) {
|
||||
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
|
||||
HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
|
||||
HttpUtil.set100ContinueExpected(request, true);
|
||||
|
||||
channel.writeInbound(request);
|
||||
HttpResponse response = channel.readOutbound();
|
||||
|
||||
assertThat(response.status(), is(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE));
|
||||
ReferenceCountUtil.release(response);
|
||||
|
||||
// request was swallowed
|
||||
assertTrue(channel.inboundMessages().isEmpty());
|
||||
assertFalse(channel.finishAndReleaseAll());
|
||||
}
|
||||
}
|
@ -45,9 +45,6 @@ public class HttpHelloWorldServerHandler extends ChannelInboundHandlerAdapter {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest req = (HttpRequest) msg;
|
||||
|
||||
if (HttpUtil.is100ContinueExpected(req)) {
|
||||
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
|
||||
}
|
||||
boolean keepAlive = HttpUtil.isKeepAlive(req);
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(CONTENT));
|
||||
response.headers().set(CONTENT_TYPE, "text/plain");
|
||||
|
@ -19,6 +19,7 @@ import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
|
||||
public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> {
|
||||
@ -36,6 +37,7 @@ public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketCh
|
||||
p.addLast(sslCtx.newHandler(ch.alloc()));
|
||||
}
|
||||
p.addLast(new HttpServerCodec());
|
||||
p.addLast(new HttpServerExpectContinueHandler());
|
||||
p.addLast(new HttpHelloWorldServerHandler());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user