Repository: netty/netty
Issue: More values other than chunked defined in Transfer-Encoding header leads to decode failure

According to https://tools.ietf.org/html/rfc7230#page-29, people can specify multiple values for `Transfer-Encoding` HTTP header,  like: `Transfer-Encoding: gzip, chunked`, but Netty does not recognize it as a chunked request in this case.

### Expected behavior
There is no exception in server, and client should get the server response.

### Actual behavior
Exception found in server:

```
java.lang.IllegalArgumentException: text is empty (possibly HTTP/0.9)
	at io.netty.handler.codec.http.HttpVersion.valueOf(HttpVersion.java:64)
	at io.netty.handler.codec.http.HttpRequestDecoder.createMessage(HttpRequestDecoder.java:87)
	at io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:206)
	at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:501)
	at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:440)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:748)
```
And there is no response back in that case.

### Steps to reproduce
Please refer to the reproducer in the link below.

### Minimal yet complete reproducer code (or URL to code)
Reproducer link: https://github.com/gaol/tests/tree/master/netty/transfer-encoding-gzip-chunked

### Netty version
* 4.1.49.Final
* latest `4.1` branch(commit: 0375e6e01b0b6412c2369a8d3e2a990f41a9edfa)

### JVM version (e.g. `java -version`)
```
[lgao@lgao netty]$ java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.222-b10, mixed mode)
```
### OS version (e.g. `uname -a`)
```
[lgao@lgao netty]$ uname -a
Linux lgao.corp.redhat.com 4.13.16-100.fc25.x86_64 #1 SMP Mon Nov 27 19:52:46 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
```

### Linked Issue
https://github.com/eclipse-vertx/vert.x/issues/3353

Issue comments:

Pull request review comments:
you need to call `c.release();`
nit: move the `;` one line up
Done by `channel.releaseInbound()`
Done
Consider writing the full HTTP request (including one or two chunks + last (zero) chunk + some trailers) to make sure that decoder's state machine transitions to the correct state and handles the entire request correctly.
Done by move the tests to `HttpContentDecoderTest` to use the pre-defined `gzipped hello world` for testing.
This can be changed to the following code to reduce the `trim()` operations and also reduce allocations:

```java
int idx = transferEncoding.indexOf(",");
if (idx != -1) {
    contentEncoding = transferEncoding.subString(0, idx).trim();
} else {
    contentEncoding = transferEncoding.trim();
}
```
Consider using `HttpHeaderNames.CONTENT_LENGTH` constant.
Looks like too many `\r\n`. Should we have only 2 pairs instead of 3?
Consider using constants from `HttpHeaderNames` and `HttpHeaderValues`.
Done
Thanks for the advice, Done
Done
Yeah, that is right. thanks for pointing out. done

Patch that resolved the issue:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java
index d2513e4a5a8..686d03257e8 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java
@@ -85,7 +85,17 @@ protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> ou
                 if (contentEncoding != null) {
                     contentEncoding = contentEncoding.trim();
                 } else {
-                    contentEncoding = IDENTITY;
+                    String transferEncoding = headers.get(HttpHeaderNames.TRANSFER_ENCODING);
+                    if (transferEncoding != null) {
+                        int idx = transferEncoding.indexOf(",");
+                        if (idx != -1) {
+                            contentEncoding = transferEncoding.substring(0, idx).trim();
+                        } else {
+                            contentEncoding = transferEncoding.trim();
+                        }
+                    } else {
+                        contentEncoding = IDENTITY;
+                    }
                 }
                 decoder = newContentDecoder(contentEncoding);
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
index 31fd14d2e5d..bb4d57cb54d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
@@ -300,7 +300,7 @@ public static void set100ContinueExpected(HttpMessage message, boolean expected)
      * @return True if transfer encoding is chunked, otherwise false
      */
     public static boolean isTransferEncodingChunked(HttpMessage message) {
-        return message.headers().contains(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true);
+        return message.headers().containsValue(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true);
     }
 
     /**


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java
index 24833a7f8d4..20f41eab49b 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java
@@ -563,6 +563,78 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
         assertEquals(0, content.refCnt());
     }
 
+    @Test
+    public void testTransferCodingGZIP() {
+        String requestStr = "POST / HTTP/1.1\r\n" +
+                "Content-Length: " + GZ_HELLO_WORLD.length + "\r\n" +
+                "Transfer-Encoding: gzip\r\n" +
+                "\r\n";
+        HttpRequestDecoder decoder = new HttpRequestDecoder();
+        HttpContentDecoder decompressor = new HttpContentDecompressor();
+        EmbeddedChannel channel = new EmbeddedChannel(decoder, decompressor);
+
+        channel.writeInbound(Unpooled.copiedBuffer(requestStr.getBytes()));
+        channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD));
+
+        HttpRequest request = channel.readInbound();
+        assertTrue(request.decoderResult().isSuccess());
+        assertFalse(request.headers().contains(HttpHeaderNames.CONTENT_LENGTH));
+
+        HttpContent content = channel.readInbound();
+        assertTrue(content.decoderResult().isSuccess());
+        assertEquals(HELLO_WORLD, content.content().toString(CharsetUtil.US_ASCII));
+        content.release();
+
+        LastHttpContent lastHttpContent = channel.readInbound();
+        assertTrue(lastHttpContent.decoderResult().isSuccess());
+        lastHttpContent.release();
+
+        assertHasInboundMessages(channel, false);
+        assertHasOutboundMessages(channel, false);
+        assertFalse(channel.finish());
+        channel.releaseInbound();
+    }
+
+    @Test
+    public void testTransferCodingGZIPAndChunked() {
+        String requestStr = "POST / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Content-Type: application/x-www-form-urlencoded\r\n" +
+                "Trailer: My-Trailer\r\n" +
+                "Transfer-Encoding: gzip, chunked\r\n" +
+                "\r\n";
+        HttpRequestDecoder decoder = new HttpRequestDecoder();
+        HttpContentDecoder decompressor = new HttpContentDecompressor();
+        EmbeddedChannel channel = new EmbeddedChannel(decoder, decompressor);
+
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
+
+        String chunkLength = Integer.toHexString(GZ_HELLO_WORLD.length);
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(chunkLength + "\r\n", CharsetUtil.US_ASCII)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("\r\n".getBytes(CharsetUtil.US_ASCII))));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("0\r\n", CharsetUtil.US_ASCII)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("My-Trailer: 42\r\n\r\n", CharsetUtil.US_ASCII)));
+
+        HttpRequest request = channel.readInbound();
+        assertTrue(request.decoderResult().isSuccess());
+        assertTrue(request.headers().containsValue(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true));
+        assertFalse(request.headers().contains(HttpHeaderNames.CONTENT_LENGTH));
+
+        HttpContent chunk1 = channel.readInbound();
+        assertTrue(chunk1.decoderResult().isSuccess());
+        assertEquals(HELLO_WORLD, chunk1.content().toString(CharsetUtil.US_ASCII));
+        chunk1.release();
+
+        LastHttpContent chunk2 = channel.readInbound();
+        assertTrue(chunk2.decoderResult().isSuccess());
+        assertEquals("42", chunk2.trailingHeaders().get("My-Trailer"));
+        chunk2.release();
+
+        assertFalse(channel.finish());
+        channel.releaseInbound();
+    }
+
     private static byte[] gzDecompress(byte[] input) {
         ZlibDecoder decoder = ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP);
         EmbeddedChannel channel = new EmbeddedChannel(decoder);