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 9a7d0b3b2d..e6bb50be7e 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 @@ -74,7 +74,20 @@ public abstract class HttpObjectEncoder extends MessageTo buf.writeBytes(CRLF); state = HttpHeaderUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK; } + + // Bypass the encoder in case of an empty buffer, so that the following idiom works: + // + // ch.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + // + // See https://github.com/netty/netty/issues/2983 for more information. + + if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) { + out.add(EMPTY_BUFFER); + return; + } + if (msg instanceof HttpContent || msg instanceof ByteBuf || msg instanceof FileRegion) { + if (state == ST_INIT) { throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg)); } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseEncoderTest.java index a1f51e3333..bf9f99d4ea 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseEncoderTest.java @@ -16,6 +16,7 @@ package io.netty.handler.codec.http; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.FileRegion; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.util.CharsetUtil; @@ -24,6 +25,7 @@ import org.junit.Test; import java.io.IOException; import java.nio.channels.WritableByteChannel; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; public class HttpResponseEncoderTest { @@ -119,4 +121,28 @@ public class HttpResponseEncoderTest { return false; } } + + @Test + public void testEmptyBufferBypass() throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder()); + + // Test writing an empty buffer works when the encoder is at ST_INIT. + channel.writeOutbound(Unpooled.EMPTY_BUFFER); + ByteBuf buffer = channel.readOutbound(); + assertThat(buffer, is(sameInstance(Unpooled.EMPTY_BUFFER))); + + // Leave the ST_INIT state. + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + assertTrue(channel.writeOutbound(response)); + buffer = channel.readOutbound(); + assertEquals("HTTP/1.1 200 OK\r\n\r\n", buffer.toString(CharsetUtil.US_ASCII)); + buffer.release(); + + // Test writing an empty buffer works when the encoder is not at ST_INIT. + channel.writeOutbound(Unpooled.EMPTY_BUFFER); + buffer = channel.readOutbound(); + assertThat(buffer, is(sameInstance(Unpooled.EMPTY_BUFFER))); + + assertFalse(channel.finish()); + } }