Repository: netty/netty
Issue: Sending an empty String like "" causes an error

### Expected behavior

### Actual behavior

### Steps to reproduce

### Minimal yet complete reproducer code (or URL to code)

### Netty version

### JVM version (e.g. `java -version`)

### OS version (e.g. `uname -a`)


Issue comments:
Netty 4.1.32

Client (browser):
`ws.send("")`
Server log:
`io.netty.handler.codec.DecoderException: io.netty.handler.codec.CodecException: cannot read uncompressed buffer
	at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:98)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:323)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:297)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:656)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:591)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:508)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:470)
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:909)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:748)
Caused by: io.netty.handler.codec.CodecException: cannot read uncompressed buffer
	at io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder.decode(DeflateDecoder.java:89)
	at io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateDecoder.decode(PerMessageDeflateDecoder.java:64)
	at io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateDecoder.decode(PerMessageDeflateDecoder.java:30)
	at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:88)
	... 20 more`
It seems that something is wrong with your codec handlers, would you please show use your codes about `ChannelIntializer` ?
Hi, This is behavior can reproduced if send 0 byte via Chrome webSocket connection, received frame is marked as compressed but payload contain only 0 byte. I think we need add test case and check spec https://tools.ietf.org/html/rfc7692 point 7.2, @normanmaurer WDYT ?
@amizurov agree...
> It seems that something is wrong with your codec handlers, would you please show use your codes about `ChannelIntializer` ?

```
package websocketx;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.ssl.SslContext;

/**
 */
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {

    private static final String WEBSOCKET_PATH = "/websocket";

    private final SslContext sslCtx;

    public WebSocketServerInitializer(SslContext sslCtx) {
        this.sslCtx = sslCtx;
    }

    @Override
    public void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();
        if (sslCtx != null) {
            pipeline.addLast(sslCtx.newHandler(ch.alloc()));
        }
        pipeline.addLast("httpServerCodec", new HttpServerCodec());
        pipeline.addLast(new HttpObjectAggregator(65536));
        pipeline.addLast(new WebSocketServerCompressionHandler());
        pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));
        pipeline.addLast(new WebSocketFrameHandler());
        pipeline.addLast(new HttpServerExpectContinueHandler());
        pipeline.addLast(new HttpServerHandler());
    }
}
```
Sorry for long wait, we incorrectly handle the section https://tools.ietf.org/html/rfc7692#section-7.2.3.6 for both situation encode/decode, I'll try to do pull request a soon as possible.
Thanks a lot

> Am 23.08.2019 um 07:47 schrieb Andrey Mizurov <notifications@github.com>:
> 
> Sorry for long wait, we incorrectly handle the section https://tools.ietf.org/html/rfc7692#section-7.2.3.6 for both situation encode/decode, I'll try to do pull request a soon as possible.
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub, or mute the thread.


Pull request review comments:
final 
maybe we can inline this into the condition where it is used?

`if (readable && compositeDecompressedContent.numComponents() <= 0 && !EMPTY_DEFLATE_BLOCK.equals(msg.content())) {`
Done for both encoder/decoder
```
decoder.writeInbound(msg.content().retain());
        if (appendFrameTail(msg)) {
            decoder.writeInbound(FRAME_TAIL.duplicate());
        }
```
after decompression msg.content() is not readable.

Patch that resolved the issue:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java
index 746032dbbcb..223e2e65201 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java
@@ -44,6 +44,10 @@ abstract class DeflateDecoder extends WebSocketExtensionDecoder {
             Unpooled.wrappedBuffer(new byte[] {0x00, 0x00, (byte) 0xff, (byte) 0xff}))
             .asReadOnly();
 
+    static final ByteBuf EMPTY_DEFLATE_BLOCK = Unpooled.unreleasableBuffer(
+            Unpooled.wrappedBuffer(new byte[] { 0x00 }))
+            .asReadOnly();
+
     private final boolean noContext;
     private final WebSocketExtensionFilter extensionDecoderFilter;
 
@@ -73,6 +77,35 @@ protected WebSocketExtensionFilter extensionDecoderFilter() {
 
     @Override
     protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
+        final ByteBuf decompressedContent = decompressContent(ctx, msg);
+
+        final WebSocketFrame outMsg;
+        if (msg instanceof TextWebSocketFrame) {
+            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent);
+        } else if (msg instanceof BinaryWebSocketFrame) {
+            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent);
+        } else if (msg instanceof ContinuationWebSocketFrame) {
+            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent);
+        } else {
+            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
+        }
+
+        out.add(outMsg);
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        cleanup();
+        super.handlerRemoved(ctx);
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        cleanup();
+        super.channelInactive(ctx);
+    }
+
+    private ByteBuf decompressContent(ChannelHandlerContext ctx, WebSocketFrame msg) {
         if (decoder == null) {
             if (!(msg instanceof TextWebSocketFrame) && !(msg instanceof BinaryWebSocketFrame)) {
                 throw new CodecException("unexpected initial frame type: " + msg.getClass().getName());
@@ -81,12 +114,14 @@ protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object
         }
 
         boolean readable = msg.content().isReadable();
+        boolean emptyDeflateBlock = EMPTY_DEFLATE_BLOCK.equals(msg.content());
+
         decoder.writeInbound(msg.content().retain());
         if (appendFrameTail(msg)) {
             decoder.writeInbound(FRAME_TAIL.duplicate());
         }
 
-        CompositeByteBuf compositeUncompressedContent = ctx.alloc().compositeBuffer();
+        CompositeByteBuf compositeDecompressedContent = ctx.alloc().compositeBuffer();
         for (;;) {
             ByteBuf partUncompressedContent = decoder.readInbound();
             if (partUncompressedContent == null) {
@@ -96,12 +131,12 @@ protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object
                 partUncompressedContent.release();
                 continue;
             }
-            compositeUncompressedContent.addComponent(true, partUncompressedContent);
+            compositeDecompressedContent.addComponent(true, partUncompressedContent);
         }
         // Correctly handle empty frames
         // See https://github.com/netty/netty/issues/4348
-        if (readable && compositeUncompressedContent.numComponents() <= 0) {
-            compositeUncompressedContent.release();
+        if (!emptyDeflateBlock && readable && compositeDecompressedContent.numComponents() <= 0) {
+            compositeDecompressedContent.release();
             throw new CodecException("cannot read uncompressed buffer");
         }
 
@@ -109,30 +144,7 @@ protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object
             cleanup();
         }
 
-        WebSocketFrame outMsg;
-        if (msg instanceof TextWebSocketFrame) {
-            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent);
-        } else if (msg instanceof BinaryWebSocketFrame) {
-            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent);
-        } else if (msg instanceof ContinuationWebSocketFrame) {
-            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), newRsv(msg),
-                    compositeUncompressedContent);
-        } else {
-            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
-        }
-        out.add(outMsg);
-    }
-
-    @Override
-    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-        cleanup();
-        super.handlerRemoved(ctx);
-    }
-
-    @Override
-    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-        cleanup();
-        super.channelInactive(ctx);
+        return compositeDecompressedContent;
     }
 
     private void cleanup() {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java
index 07d5ca34bc4..1203c498804 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java
@@ -82,8 +82,39 @@ protected WebSocketExtensionFilter extensionEncoderFilter() {
     protected abstract boolean removeFrameTail(WebSocketFrame msg);
 
     @Override
-    protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg,
-            List<Object> out) throws Exception {
+    protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
+        final ByteBuf compressedContent;
+        if (msg.content().isReadable()) {
+            compressedContent = compressContent(ctx, msg);
+        } else if (msg.isFinalFragment()) {
+            // Set empty DEFLATE block manually for unknown buffer size
+            // https://tools.ietf.org/html/rfc7692#section-7.2.3.6
+            compressedContent = EMPTY_DEFLATE_BLOCK.duplicate();
+        } else {
+            throw new CodecException("cannot compress content buffer");
+        }
+
+        final WebSocketFrame outMsg;
+        if (msg instanceof TextWebSocketFrame) {
+            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
+        } else if (msg instanceof BinaryWebSocketFrame) {
+            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
+        } else if (msg instanceof ContinuationWebSocketFrame) {
+            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
+        } else {
+            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
+        }
+
+        out.add(outMsg);
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        cleanup();
+        super.handlerRemoved(ctx);
+    }
+
+    private ByteBuf compressContent(ChannelHandlerContext ctx, WebSocketFrame msg) {
         if (encoder == null) {
             encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder(
                     ZlibWrapper.NONE, compressionLevel, windowSize, 8));
@@ -103,6 +134,7 @@ protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg,
             }
             fullCompressedContent.addComponent(true, partCompressedContent);
         }
+
         if (fullCompressedContent.numComponents() <= 0) {
             fullCompressedContent.release();
             throw new CodecException("cannot read compressed buffer");
@@ -120,23 +152,7 @@ protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg,
             compressedContent = fullCompressedContent;
         }
 
-        WebSocketFrame outMsg;
-        if (msg instanceof TextWebSocketFrame) {
-            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
-        } else if (msg instanceof BinaryWebSocketFrame) {
-            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
-        } else if (msg instanceof ContinuationWebSocketFrame) {
-            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
-        } else {
-            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
-        }
-        out.add(outMsg);
-    }
-
-    @Override
-    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-        cleanup();
-        super.handlerRemoved(ctx);
+        return compressedContent;
     }
 
     private void cleanup() {


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java
index fc872b1ecc4..fcd290ab9db 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java
@@ -33,6 +33,7 @@
 import java.util.Random;
 
 import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*;
+import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder.*;
 import static io.netty.util.CharsetUtil.*;
 import static org.junit.Assert.*;
 
@@ -310,4 +311,21 @@ public boolean mustSkip(WebSocketFrame frame) {
         }
     }
 
+    @Test
+    public void testEmptyFrameDecompression() {
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
+
+        TextWebSocketFrame emptyDeflateBlockFrame = new TextWebSocketFrame(true, WebSocketExtension.RSV1,
+                                                                           EMPTY_DEFLATE_BLOCK);
+
+        assertTrue(decoderChannel.writeInbound(emptyDeflateBlockFrame));
+        TextWebSocketFrame emptyBufferFrame = decoderChannel.readInbound();
+
+        assertFalse(emptyBufferFrame.content().isReadable());
+
+        // Composite empty buffer
+        assertTrue(emptyBufferFrame.release());
+        assertFalse(decoderChannel.finish());
+    }
+
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java
index 9e986511c57..1f8b47744a7 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java
@@ -34,6 +34,7 @@
 import java.util.Random;
 
 import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*;
+import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder.*;
 import static io.netty.util.CharsetUtil.*;
 import static org.junit.Assert.*;
 
@@ -102,7 +103,7 @@ public void testAlreadyCompressedFrame() {
     }
 
     @Test
-    public void testFramementedFrame() {
+    public void testFragmentedFrame() {
         EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false,
                                                                                           NEVER_SKIP));
         EmbeddedChannel decoderChannel = new EmbeddedChannel(
@@ -272,4 +273,36 @@ public boolean mustSkip(WebSocketFrame frame) {
         }
     }
 
+    @Test
+    public void testEmptyFrameCompression() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
+
+        TextWebSocketFrame emptyFrame = new TextWebSocketFrame("");
+
+        assertTrue(encoderChannel.writeOutbound(emptyFrame));
+        TextWebSocketFrame emptyDeflateFrame = encoderChannel.readOutbound();
+
+        assertEquals(WebSocketExtension.RSV1, emptyDeflateFrame.rsv());
+        assertTrue(ByteBufUtil.equals(EMPTY_DEFLATE_BLOCK, emptyDeflateFrame.content()));
+        // Unreleasable buffer
+        assertFalse(emptyDeflateFrame.release());
+
+        assertFalse(encoderChannel.finish());
+    }
+
+    @Test(expected = EncoderException.class)
+    public void testCodecExceptionForNotFinEmptyFrame() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
+
+        TextWebSocketFrame emptyNotFinFrame = new TextWebSocketFrame(false, 0, "");
+
+        try {
+            encoderChannel.writeOutbound(emptyNotFinFrame);
+        } finally {
+            // EmptyByteBuf buffer
+            assertFalse(emptyNotFinFrame.release());
+            assertFalse(encoderChannel.finish());
+        }
+    }
+
 }