Repository: netty/netty (Java)

Issue:
JSON in POST request treated as body attribute in HttpPostStandardRequestDecoder
### Expected behavior

JSON in POST request shouldn't be counted as body attribute in `HttpPostStandardRequestDecoder`, as in releases `<4.1.108-Final` (checked on `4.1.107.Final` and `4.1.106.Final`)

### Actual behavior

JSON in POST request is treated as body attribute in `HttpPostStandardRequestDecoder`, and an attribute is created with key equal to JSON and empty value

### Steps to reproduce

Send a POST request to netty `>=4.1.108-Final`, decode the request using `HttpPostRequestDecoder` and check `.getBodyHttpDatas() ` The resulting list will contain Attribute HttpData where the key is a JSON sent by a client, and value is empty

### Minimal yet complete reproducer code (or URL to code)

```
public class NettyReproduction {
    public static void main(String[] args) {
        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/netty/reproduction",
            Unpooled.wrappedBuffer("""
                {
                    "netty": "reproduction"
                }
                """.getBytes(StandardCharsets.UTF_8)));
        var decoder = new HttpPostRequestDecoder(request);
        var result = decoder.getBodyHttpDatas();
        System.out.println(result); 
    }
}
```
results in:
`[Mixed: {    "netty": "reproduction"}=]` on `4.1.109.Final` and `4.1.108.Final` 

but results in:
`[]` on `4.1.107.Final` and `4.1.106.Final`


### Netty version
as above

### JVM version (e.g. `java -version`)
Java 21

### OS version (e.g. `uname -a`)
Windows 10, 
and @Rufusmen reproduced on Linux (Ubuntu 22.04.4 LTS x86_64)


We suspect that the reason for such change is this PR: https://github.com/netty/netty/pull/13908 , thus we are wondering if such behaviour is desired, if the decoder decodes something different than `x-www-form-urlencoded`



Hints:

Patch:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
@@ -17,12 +17,13 @@
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
-import io.netty.handler.codec.DecoderException;
 import io.netty.handler.codec.http.HttpConstants;
 import io.netty.handler.codec.http.HttpContent;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.handler.codec.http.QueryStringDecoder;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
 import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
 import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
@@ -471,7 +472,8 @@ private void parseBodyAttributesStandard() {
                                 charset);
                         currentAttribute = factory.createAttribute(request, key);
                         firstpos = currentpos;
-                    } else if (read == '&' || (isLastChunk && !undecodedChunk.isReadable())) { // special empty FIELD
+                    } else if (read == '&' ||
+                            (isLastChunk && !undecodedChunk.isReadable() && hasFormBody())) { // special empty FIELD
                         currentStatus = MultiPartStatus.DISPOSITION;
                         ampersandpos = read == '&' ? currentpos - 1 : currentpos;
                         String key = decodeAttribute(
@@ -597,7 +599,8 @@ private void parseBodyAttributes() {
                                 charset);
                         currentAttribute = factory.createAttribute(request, key);
                         firstpos = currentpos;
-                    } else if (read == '&' || (isLastChunk && !undecodedChunk.isReadable())) { // special empty FIELD
+                    } else if (read == '&' ||
+                            (isLastChunk && !undecodedChunk.isReadable() && hasFormBody())) { // special empty FIELD
                         currentStatus = MultiPartStatus.DISPOSITION;
                         ampersandpos = read == '&' ? currentpos - 1 : currentpos;
                         String key = decodeAttribute(
@@ -783,6 +786,18 @@ public void removeHttpDataFromClean(InterfaceHttpData data) {
         factory.removeHttpDataFromClean(request, data);
     }
 
+    /**
+     * Check if request has headers indicating that it contains form body
+     */
+    private boolean hasFormBody() {
+        String contentHeaderValue = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
+        if (contentHeaderValue == null) {
+            return false;
+        }
+        return HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.contentEquals(contentHeaderValue)
+                || HttpHeaderValues.MULTIPART_FORM_DATA.contentEquals(contentHeaderValue);
+    }
+
     private static final class UrlEncodedDetector implements ByteProcessor {
         @Override
         public boolean process(byte value) throws Exception {


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoderTest.java
--- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoderTest.java
@@ -27,6 +27,7 @@
 import io.netty.util.CharsetUtil;
 import org.junit.jupiter.api.Test;
 
+import static io.netty.handler.codec.http.DefaultHttpHeadersFactory.headersFactory;
 import static org.junit.jupiter.api.Assertions.*;
 
 class HttpPostStandardRequestDecoderTest {
@@ -52,7 +53,8 @@ void testDecodeAttributes() {
     void testDecodeSingleAttributeWithNoValue() {
         String requestBody = "key1";
 
-        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload");
+        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload",
+                headersFactory().newHeaders().add("Content-Type", "application/x-www-form-urlencoded"));
 
         HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(httpDiskDataFactory(), request);
         ByteBuf buf = Unpooled.wrappedBuffer(requestBody.getBytes(CharsetUtil.UTF_8));
@@ -68,7 +70,8 @@ void testDecodeSingleAttributeWithNoValue() {
     void testDecodeSingleAttributeWithNoValueEmptyLast() {
         String requestBody = "key1";
 
-        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload");
+        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload",
+                headersFactory().newHeaders().add("Content-Type", "application/x-www-form-urlencoded"));
 
         HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(httpDiskDataFactory(), request);
         ByteBuf buf = Unpooled.wrappedBuffer(requestBody.getBytes(CharsetUtil.UTF_8));
@@ -86,7 +89,8 @@ void testDecodeSingleAttributeWithNoValueEmptyLast() {
     void testDecodeEndAttributeWithNoValue() {
         String requestBody = "key1=value1&key2";
 
-        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload");
+        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload",
+                headersFactory().newHeaders().add("Content-Type", "application/x-www-form-urlencoded"));
 
         HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(httpDiskDataFactory(), request);
         ByteBuf buf = Unpooled.wrappedBuffer(requestBody.getBytes(CharsetUtil.UTF_8));
@@ -99,6 +103,37 @@ void testDecodeEndAttributeWithNoValue() {
         decoder.destroy();
     }
 
+    @Test
+    void testDecodeJsonAttributeAsEmpty() {
+        String requestBody = "{\"iAm\": \" a JSON!\"}";
+
+        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload",
+                headersFactory().newHeaders().add("Content-Type", "application/json"));
+
+        HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(httpDiskDataFactory(), request);
+        ByteBuf buf = Unpooled.wrappedBuffer(requestBody.getBytes(CharsetUtil.UTF_8));
+        DefaultHttpContent httpContent = new DefaultLastHttpContent(buf);
+        decoder.offer(httpContent);
+
+        assertEquals(0, decoder.getBodyHttpDatas().size());
+        decoder.destroy();
+    }
+
+    @Test
+    void testDecodeJsonAttributeAsEmptyAndNoHeaders() {
+        String requestBody = "{\"iAm\": \" a JSON!\"}";
+
+        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload");
+
+        HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(httpDiskDataFactory(), request);
+        ByteBuf buf = Unpooled.wrappedBuffer(requestBody.getBytes(CharsetUtil.UTF_8));
+        DefaultHttpContent httpContent = new DefaultLastHttpContent(buf);
+        decoder.offer(httpContent);
+
+        assertEquals(0, decoder.getBodyHttpDatas().size());
+        decoder.destroy();
+    }
+
     @Test
     void testDecodeStartAttributeWithNoValue() {
         String requestBody = "key1&key2=value2";
@@ -120,7 +155,8 @@ void testDecodeStartAttributeWithNoValue() {
     void testDecodeMultipleAttributesWithNoValue() {
         String requestBody = "key1&key2&key3";
 
-        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload");
+        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload",
+                headersFactory().newHeaders().add("Content-Type", "application/x-www-form-urlencoded"));
 
         HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(httpDiskDataFactory(), request);
         ByteBuf buf = Unpooled.wrappedBuffer(requestBody.getBytes(CharsetUtil.UTF_8));