Repository: netty/netty
Issue: Sending DefaultLastContent with trailers causes "io.netty.handler.codec.EncoderException: HttpServerCodec$HttpServerResponseEncoder must produce at least one message."
Labels: defect

### Expected behavior
Writing `DefaultLastContent` object with trailing headers sends an empty chunk with trailing headers.

### Actual behavior
Writing `DefaultLastContent` with trailing headers causes EncoderException:
```
io.netty.handler.codec.EncoderException: HttpServerCodec$HttpServerResponseEncoder must produce at least one message.
        at io.netty.handler.codec.MessageToMessageEncoder.write(MessageToMessageEncoder.java:98) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.CombinedChannelDuplexHandler.write(CombinedChannelDuplexHandler.java:348) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:732) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:724) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:809) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:717) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.handler.logging.LoggingHandler.write(LoggingHandler.java:254) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:732) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeWriteAndFlush(AbstractChannelHandlerContext.java:795) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:807) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:788) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:824) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
        at com.datastax.bdp.fs.rest.util.NettyHttpUtil$.writeAndFlush(NettyHttpUtil.scala:77) ~[dse-dsefs-common-5.0.12-cbbffcf.jar:5.0.12-cbbffcf]
        at com.datastax.bdp.fs.rest.server.RestServerHandler$$anonfun$sendResponseBody$2.apply(RestServerHandler.scala:163) [dse-dsefs-common-5.0.12-cbbffcf.jar:5.0.12-cbbffcf]
        at com.datastax.bdp.fs.rest.server.RestServerHandler$$anonfun$sendResponseBody$2.apply(RestServerHandler.scala:143) [dse-dsefs-common-5.0.12-cbbffcf.jar:5.0.12-cbbffcf]
        at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:32) [scala-library-2.10.6.jar:na]
        at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:399) [netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:464) [netty-all-4.0.52.Final.jar:4.0.52.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:131) [netty-all-4.0.52.Final.jar:4.0.52.Final]
        at java.lang.Thread.run(Thread.java:748) [na:1.8.0_144]

```
Writing `DefaultLastContent` with no trailing headers in the same place of code, works as expected - sends an empty last chunk and no exception is thrown.

### Steps to reproduce
1. Setup a simple HttpServer using HttpServerCodec (without HttpMessageAggregator)
2. When sending a response, enable chunked transfer encoding.
3. writeAndFlush a DefaultLastContent object with trailers:
```
   val lastContent = new DefaultLastHttpContent()
   lastContent.trailingHeaders().add(...., ....)
   context.writeAndFlush(lastContent)
```

### Minimal yet complete reproducer code (or URL to code)

### Netty version
We found the problem after upgrading to 4.0.52.Final
Error was not present in 4.0.34.Final.

### JVM version (e.g. `java -version`)
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

### OS version (e.g. `uname -a`)
Linux p5520 4.14.0-041400rc8-generic #201711052313 SMP Sun Nov 5 23:14:08 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux


Issue comments:
Will have a Look

> Am 18.11.2017 um 23:03 schrieb Piotr Kołaczkowski <notifications@github.com>:
> 
> Expected behavior
> 
> Writing DefaultLastContent object with trailing headers sends an empty chunk with trailing headers.
> 
> Actual behavior
> 
> Writing DefaultLastContent with trailing headers causes EncoderException:
> 
> io.netty.handler.codec.EncoderException: HttpServerCodec$HttpServerResponseEncoder must produce at least one message.
>         at io.netty.handler.codec.MessageToMessageEncoder.write(MessageToMessageEncoder.java:98) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.CombinedChannelDuplexHandler.write(CombinedChannelDuplexHandler.java:348) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:732) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:724) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:809) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:717) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.handler.logging.LoggingHandler.write(LoggingHandler.java:254) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:732) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.invokeWriteAndFlush(AbstractChannelHandlerContext.java:795) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:807) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:788) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:824) ~[netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at com.datastax.bdp.fs.rest.util.NettyHttpUtil$.writeAndFlush(NettyHttpUtil.scala:77) ~[dse-dsefs-common-5.0.12-cbbffcf.jar:5.0.12-cbbffcf]
>         at com.datastax.bdp.fs.rest.server.RestServerHandler$$anonfun$sendResponseBody$2.apply(RestServerHandler.scala:163) [dse-dsefs-common-5.0.12-cbbffcf.jar:5.0.12-cbbffcf]
>         at com.datastax.bdp.fs.rest.server.RestServerHandler$$anonfun$sendResponseBody$2.apply(RestServerHandler.scala:143) [dse-dsefs-common-5.0.12-cbbffcf.jar:5.0.12-cbbffcf]
>         at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:32) [scala-library-2.10.6.jar:na]
>         at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:399) [netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:464) [netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:131) [netty-all-4.0.52.Final.jar:4.0.52.Final]
>         at java.lang.Thread.run(Thread.java:748) [na:1.8.0_144]
> 
> Writing DefaultLastContent with no trailing headers in the same place of code, works as expected - sends an empty last chunk and no exception is thrown.
> 
> Steps to reproduce
> 
> Setup a simple HttpServer using HttpServerCodec (without HttpMessageAggregator)
> When sending a response, enable chunked transfer encoding.
> writeAndFlush a DefaultLastContent object with trailers:
>    val lastContent = new DefaultLastHttpContent()
>    lastContent.trailingHeaders().add(...., ....)
>    context.writeAndFlush(lastContent)
> Minimal yet complete reproducer code (or URL to code)
> 
> Netty version
> 
> We found the problem after upgrading to 4.0.52.Final
> Error was not present in 4.0.34.Final.
> 
> JVM version (e.g. java -version)
> 
> java version "1.8.0_144"
> Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
> Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
> 
> OS version (e.g. uname -a)
> 
> Linux p5520 4.14.0-041400rc8-generic #201711052313 SMP Sun Nov 5 23:14:08 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub, or mute the thread.
> 

Fixed by 
8009c4c

Pull request review comments:

Patch that resolved the issue:
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 2bdb6ce78ec..50b3ab76731 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
@@ -206,6 +206,7 @@ private void encodeChunkedContent(ChannelHandlerContext ctx, Object msg, long co
                 buf.writeBytes(CRLF);
                 trailersEncodedSizeAccumulator = TRAILERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
                                                  TRAILERS_WEIGHT_HISTORICAL * trailersEncodedSizeAccumulator;
+                out.add(buf);
             }
         } else if (contentLength == 0) {
             // Need to produce some output otherwise an


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java
index 742689f683d..ddcb007c571 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestEncoderTest.java
@@ -135,16 +135,26 @@ public void testEmptyBufferShouldPassThrough() throws Exception {
     }
 
     @Test
-    public void testEmptyContentChunked() throws Exception {
-        testEmptyContent(true);
+    public void testEmptyContentsChunked() throws Exception {
+        testEmptyContents(true, false);
     }
 
     @Test
-    public void testEmptyContentNotChunked() throws Exception {
-        testEmptyContent(false);
+    public void testEmptyContentsChunkedWithTrailers() throws Exception {
+        testEmptyContents(true, true);
     }
 
-    private void testEmptyContent(boolean chunked) throws Exception {
+    @Test
+    public void testEmptyContentsNotChunked() throws Exception {
+        testEmptyContents(false, false);
+    }
+
+    @Test
+    public void testEmptyContentNotsChunkedWithTrailers() throws Exception {
+        testEmptyContents(false, true);
+    }
+
+    private void testEmptyContents(boolean chunked, boolean trailers) throws Exception {
         HttpRequestEncoder encoder = new HttpRequestEncoder();
         EmbeddedChannel channel = new EmbeddedChannel(encoder);
         HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
@@ -157,7 +167,11 @@ private void testEmptyContent(boolean chunked) throws Exception {
         assertTrue(channel.writeOutbound(new DefaultHttpContent(contentBuffer)));
 
         ByteBuf lastContentBuffer = Unpooled.buffer();
-        assertTrue(channel.writeOutbound(new DefaultHttpContent(lastContentBuffer)));
+        LastHttpContent last = new DefaultLastHttpContent(lastContentBuffer);
+        if (trailers) {
+            last.trailingHeaders().set("X-Netty-Test", "true");
+        }
+        assertTrue(channel.writeOutbound(last));
 
         // Ensure we only produce ByteBuf instances.
         ByteBuf head = (ByteBuf) channel.readOutbound();
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 23557036a3c..41e9ea56eb6 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
@@ -286,4 +286,55 @@ private static void assertEmptyResponse(EmbeddedChannel channel, HttpResponseSta
         buffer = (ByteBuf) channel.readOutbound();
         buffer.release();
     }
+
+    @Test
+    public void testEmptyContentsChunked() throws Exception {
+        testEmptyContents(true, false);
+    }
+
+    @Test
+    public void testEmptyContentsChunkedWithTrailers() throws Exception {
+        testEmptyContents(true, true);
+    }
+
+    @Test
+    public void testEmptyContentsNotChunked() throws Exception {
+        testEmptyContents(false, false);
+    }
+
+    @Test
+    public void testEmptyContentNotsChunkedWithTrailers() throws Exception {
+        testEmptyContents(false, true);
+    }
+
+    private void testEmptyContents(boolean chunked, boolean trailers) throws Exception {
+        HttpResponseEncoder encoder = new HttpResponseEncoder();
+        EmbeddedChannel channel = new EmbeddedChannel(encoder);
+        HttpResponse request = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+        if (chunked) {
+            HttpHeaders.setTransferEncodingChunked(request);
+        }
+        assertTrue(channel.writeOutbound(request));
+
+        ByteBuf contentBuffer = Unpooled.buffer();
+        assertTrue(channel.writeOutbound(new DefaultHttpContent(contentBuffer)));
+
+        ByteBuf lastContentBuffer = Unpooled.buffer();
+        LastHttpContent last = new DefaultLastHttpContent(lastContentBuffer);
+        if (trailers) {
+            last.trailingHeaders().set("X-Netty-Test", "true");
+        }
+        assertTrue(channel.writeOutbound(last));
+
+        // Ensure we only produce ByteBuf instances.
+        ByteBuf head = (ByteBuf) channel.readOutbound();
+        assertTrue(head.release());
+
+        ByteBuf content = (ByteBuf) channel.readOutbound();
+        content.release();
+
+        ByteBuf lastContent = (ByteBuf) channel.readOutbound();
+        lastContent.release();
+        assertFalse(channel.finish());
+    }
 }