Repository: netty/netty
Issue: Unexpected IllegalReferenceCountException on decode multipart request

### Actual behavior
```
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
	at io.netty.util.AbstractReferenceCounted.release0(AbstractReferenceCounted.java:87)
	at io.netty.util.AbstractReferenceCounted.release(AbstractReferenceCounted.java:71)
	at io.netty.handler.codec.http.multipart.MixedAttribute.release(MixedAttribute.java:320)
	at io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder.destroy(HttpPostMultipartRequestDecoder.java:947)
	at io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.destroy(HttpPostRequestDecoder.java:247)
```

### Steps to reproduce
```java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import org.junit.Test;

import java.io.IOException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class HttpMultipartRequestTest {
    
    String getPostParam(HttpPostRequestDecoder requestDecoder, String name) {
        final InterfaceHttpData data = requestDecoder.getBodyHttpData(name);

        if (data != null && InterfaceHttpData.HttpDataType.Attribute.equals(data.getHttpDataType())) {
            final Attribute attr = (Attribute)data;

            try {
                return attr.getValue();
            } catch (IOException e) {
                return null;
            }
        }

        return null;
    }

    @Test
    public void testMultipartRequest() {
        ByteBuf byteBuf = Unpooled.wrappedBuffer(("------------------------------01f136d9282f\n" +
                "Content-Disposition: form-data; name=\"msg_id\"\n" +
                "\n" +
                "15200\n" +
                "------------------------------01f136d9282f\n" +
                "Content-Disposition: form-data; name=\"msg\"\n" +
                "\n" +
                "test message\n" +
                "------------------------------01f136d9282f--").getBytes());

        DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.POST, "/push_data", byteBuf);
        req.headers().add("Content-Type", "multipart/form-data; boundary=----------------------------01f136d9282f");

        HttpPostRequestDecoder decoder =
                new HttpPostRequestDecoder(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
                        req,
                        java.nio.charset.Charset.forName("utf-8"));

        assertEquals("test message", getPostParam(decoder, "msg"));
        assertEquals("15200", getPostParam(decoder, "msg_id"));
        assertTrue(decoder.isMultipart());

        decoder.destroy();
    }
}
```
Decoder will produce IllegalReferenceCountException.

### Netty version
4.1.31.Final

### JVM version (e.g. `java -version`)
java version "1.8.0_161"

### OS version (e.g. `uname -a`)
Linux

Issue comments:
@ssserj yes this looks like a bug... are you interested in providing a fix ?
@normanmaurer I still don’t known how to fix it. I need investigate problem, find proper fix and prepare PR. I’ll try..
@ssserj @normanmaurer Hi, could I ask for the state of this, as we sometimes also encounter this in our production server?

@DATuan91 Any request of this type (see test above) produces an error. I'm looking for a fix. I note that in 4.1.16 there was no such error. In the free time I will try to compare these two branches and find the bug.
Thank you, @ssserj. Because I'm not familiar with netty codebase so I couldn't find a proper fix myself.
I have also encountered this problem when using Netty 4.1.31.Final. However, it does not occurs on some old versions like 4.1.13.Final.
I compare the two versions and try to find out the difference. The issue may be enrolled by https://github.com/netty/netty/commit/2b4f6677917cbe4121c2b748ba7002f8ba4bb8b5.
I've made some comments on this commit.


FWIW this breaks our (unfiltered) tests and I’m not sure we can do anything on our end to fix it.

Pull request review comments:
can we also add a `assertEquals(0, req.refCnt());` after the `destroy()` call ?
use `CharsetUtil.UTF_8`
nit: `FullHttpRequest req = ...`
Most probably, should be assertEquals(1, req.refCnt()) ?
nit: please revert... we usually not do wildcard imports for this kind of things 

Patch that resolved the issue:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
index a065b879e11..c3404d9780d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
@@ -931,19 +931,15 @@ protected InterfaceHttpData getFileUpload(String delimiter) {
      */
     @Override
     public void destroy() {
-        checkDestroyed();
+        // Release all data items, including those not yet pulled
         cleanFiles();
+
         destroyed = true;
 
         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
             undecodedChunk.release();
             undecodedChunk = null;
         }
-
-        // release all data which was not yet pulled
-        for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) {
-            bodyListHttpData.get(i).release();
-        }
     }
 
     /**


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
index d29f6b3cd44..fc258cc8316 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
@@ -20,16 +20,7 @@
 import io.netty.buffer.Unpooled;
 import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.handler.codec.DecoderResult;
-import io.netty.handler.codec.http.DefaultFullHttpRequest;
-import io.netty.handler.codec.http.DefaultHttpContent;
-import io.netty.handler.codec.http.DefaultHttpRequest;
-import io.netty.handler.codec.http.DefaultLastHttpContent;
-import io.netty.handler.codec.http.HttpContent;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import io.netty.handler.codec.http.HttpHeaderValues;
-import io.netty.handler.codec.http.HttpMethod;
-import io.netty.handler.codec.http.HttpVersion;
-import io.netty.handler.codec.http.LastHttpContent;
+import io.netty.handler.codec.http.*;
 import io.netty.util.CharsetUtil;
 import org.junit.Test;
 
@@ -689,4 +680,37 @@ public void testDecodeMalformedEmptyContentTypeFieldParameters() throws Exceptio
         assertEquals("tmp-0.txt", fileUpload.getFilename());
         decoder.destroy();
     }
+
+    // https://github.com/netty/netty/issues/8575
+    @Test
+    public void testMultipartRequest() throws Exception {
+        String BOUNDARY = "01f136d9282f";
+
+        ByteBuf byteBuf = Unpooled.wrappedBuffer(("--" + BOUNDARY + "\n" +
+                "Content-Disposition: form-data; name=\"msg_id\"\n" +
+                "\n" +
+                "15200\n" +
+                "--" + BOUNDARY + "\n" +
+                "Content-Disposition: form-data; name=\"msg\"\n" +
+                "\n" +
+                "test message\n" +
+                "--" + BOUNDARY + "--").getBytes());
+
+        FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.POST, "/up", byteBuf);
+        req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + BOUNDARY);
+
+        HttpPostRequestDecoder decoder =
+                new HttpPostRequestDecoder(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
+                        req,
+                        CharsetUtil.UTF_8);
+
+        assertTrue(decoder.isMultipart());
+        assertFalse(decoder.getBodyHttpDatas().isEmpty());
+        assertEquals(2, decoder.getBodyHttpDatas().size());
+        assertEquals("test message", ((Attribute) decoder.getBodyHttpData("msg")).getValue());
+        assertEquals("15200", ((Attribute) decoder.getBodyHttpData("msg_id")).getValue());
+
+        decoder.destroy();
+        assertEquals(1, req.refCnt());
+    }
 }