diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java index dba6f01de2..10794983fd 100755 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java @@ -57,6 +57,7 @@ public abstract class HttpObjectEncoder extends MessageTo private static final int ST_INIT = 0; private static final int ST_CONTENT_NON_CHUNK = 1; private static final int ST_CONTENT_CHUNK = 2; + private static final int ST_CONTENT_ALWAYS_EMPTY = 3; @SuppressWarnings("RedundantFieldInitialization") private int state = ST_INIT; @@ -77,7 +78,8 @@ public abstract class HttpObjectEncoder extends MessageTo encodeInitialLine(buf, m); encodeHeaders(m.headers(), buf); buf.writeBytes(CRLF); - state = HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK; + state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY : + HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK; } // Bypass the encoder in case of an empty buffer, so that the following idiom works: @@ -92,44 +94,50 @@ public abstract class HttpObjectEncoder extends MessageTo } if (msg instanceof HttpContent || msg instanceof ByteBuf || msg instanceof FileRegion) { - - if (state == ST_INIT) { - throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg)); - } - - final long contentLength = contentLength(msg); - if (state == ST_CONTENT_NON_CHUNK) { - if (contentLength > 0) { - if (buf != null && buf.writableBytes() >= contentLength && msg instanceof HttpContent) { - // merge into other buffer for performance reasons - buf.writeBytes(((HttpContent) msg).content()); - out.add(buf); + switch (state) { + case ST_INIT: + throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg)); + case ST_CONTENT_ALWAYS_EMPTY: + out.add(EMPTY_BUFFER); + if (msg instanceof LastHttpContent) { + state = ST_INIT; + } + return; + case ST_CONTENT_NON_CHUNK: + final long contentLength = contentLength(msg); + if (contentLength > 0) { + if (buf != null && buf.writableBytes() >= contentLength && msg instanceof HttpContent) { + // merge into other buffer for performance reasons + buf.writeBytes(((HttpContent) msg).content()); + out.add(buf); + } else { + if (buf != null) { + out.add(buf); + } + out.add(encodeAndRetain(msg)); + } } else { if (buf != null) { out.add(buf); + } else { + // Need to produce some output otherwise an + // IllegalStateException will be thrown + out.add(EMPTY_BUFFER); } - out.add(encodeAndRetain(msg)); } - } else { + + if (msg instanceof LastHttpContent) { + state = ST_INIT; + } + return; + case ST_CONTENT_CHUNK: if (buf != null) { out.add(buf); - } else { - // Need to produce some output otherwise an - // IllegalStateException will be thrown - out.add(EMPTY_BUFFER); } - } - - if (msg instanceof LastHttpContent) { - state = ST_INIT; - } - } else if (state == ST_CONTENT_CHUNK) { - if (buf != null) { - out.add(buf); - } - encodeChunkedContent(ctx, msg, contentLength, out); - } else { - throw new Error(); + encodeChunkedContent(ctx, msg, contentLength(msg), out); + return; + default: + throw new Error(); } } else { if (buf != null) { @@ -187,6 +195,10 @@ public abstract class HttpObjectEncoder extends MessageTo } } + boolean isContentAlwaysEmpty(@SuppressWarnings("unused") H msg) { + return false; + } + @Override public boolean acceptOutboundMessage(Object msg) throws Exception { return msg instanceof HttpObject || msg instanceof ByteBuf || msg instanceof FileRegion; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java index 684af0b67a..29a11992ad 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java @@ -15,9 +15,14 @@ */ package io.netty.handler.codec.http; +import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.CombinedChannelDuplexHandler; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Queue; + /** * A combination of {@link HttpRequestDecoder} and {@link HttpResponseEncoder} * which enables easier server side HTTP implementation. @@ -27,6 +32,9 @@ import io.netty.channel.CombinedChannelDuplexHandler; public final class HttpServerCodec extends CombinedChannelDuplexHandler implements HttpServerUpgradeHandler.SourceCodec { + /** A queue that is used for correlating a request and a response. */ + private final Queue queue = new ArrayDeque(); + /** * Creates a new instance with the default decoder options * ({@code maxInitialLineLength (4096}}, {@code maxHeaderSize (8192)}, and @@ -40,15 +48,16 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler out) throws Exception { + int oldSize = out.size(); + super.decode(ctx, buffer, out); + int size = out.size(); + for (int i = oldSize; i < size; i++) { + Object obj = out.get(i); + if (obj instanceof HttpRequest) { + queue.add(((HttpRequest) obj).method()); + } + } + } + } + + private final class HttpServerResponseEncoder extends HttpResponseEncoder { + + @Override + boolean isContentAlwaysEmpty(@SuppressWarnings("unused") HttpResponse msg) { + return HttpMethod.HEAD.equals(queue.poll()); + } + } } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpServerCodecTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpServerCodecTest.java index 345e9e821f..3087819894 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpServerCodecTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpServerCodecTest.java @@ -114,6 +114,37 @@ public class HttpServerCodecTest { ch.finish(); } + @Test + public void testChunkedHeadResponse() { + EmbeddedChannel ch = new EmbeddedChannel(new HttpServerCodec()); + + // Send the request headers. + assertTrue(ch.writeInbound(Unpooled.copiedBuffer( + "HEAD / HTTP/1.1\r\n\r\n", CharsetUtil.UTF_8))); + + HttpRequest request = ch.readInbound(); + assertEquals(HttpMethod.HEAD, request.method()); + LastHttpContent content = ch.readInbound(); + assertFalse(content.content().isReadable()); + content.release(); + + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + HttpUtil.setTransferEncodingChunked(response, true); + assertTrue(ch.writeOutbound(response)); + assertTrue(ch.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT)); + assertTrue(ch.finish()); + + ByteBuf buf = ch.readOutbound(); + assertEquals("HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n", buf.toString(CharsetUtil.US_ASCII)); + buf.release(); + + buf = ch.readOutbound(); + assertFalse(buf.isReadable()); + buf.release(); + + assertFalse(ch.finishAndReleaseAll()); + } + private static ByteBuf prepareDataChunk(int size) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < size; ++i) {