HttpPostStandardRequestDecoder not decoding `+` to whitespaces
The new `UrlDecoder` in `HttpPostStandardRequestDecoder` does not convert `+` to whitespaces, which `QueryStringDecoder.decodeComponent` used to do.
https://github.com/netty/netty/pull/10209/files#diff-99f86aa293d9977ca3a310e1488d31c1R713


codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
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
index 4cb82e678da..a71e2a14a2e 100644
--- 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
@@ -656,7 +656,7 @@ private static String decodeAttribute(String s, Charset charset) {
     }
 
     private static ByteBuf decodeAttribute(ByteBuf b, Charset charset) {
-        int firstEscaped = b.forEachByte(new ByteProcessor.IndexOfProcessor((byte) '%'));
+        int firstEscaped = b.forEachByte(new UrlEncodedDetector());
         if (firstEscaped == -1) {
             return null; // nothing to decode
         }
@@ -714,6 +714,13 @@ public void removeHttpDataFromClean(InterfaceHttpData data) {
         factory.removeHttpDataFromClean(request, data);
     }
 
+    private static final class UrlEncodedDetector implements ByteProcessor {
+        @Override
+        public boolean process(byte value) throws Exception {
+            return value != '%' && value != '+';
+        }
+    }
+
     private static final class UrlDecoder implements ByteProcessor {
 
         private final ByteBuf output;
@@ -742,6 +749,8 @@ public boolean process(byte value) {
                 }
             } else if (value == '%') {
                 nextEscapedIdx = 1;
+            } else if (value == '+') {
+                output.writeByte(' ');
             } else {
                 output.writeByte(value);
             }

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 7efdc55849e..02958d46aa1 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
@@ -829,7 +829,7 @@ public void testDecodeWithLanguageContentDispositionFieldParametersForFix() thro
 
     @Test
     public void testDecodeFullHttpRequestWithUrlEncodedBody() throws Exception {
-        byte[] bodyBytes = "foo=bar&a=b&empty=&city=%3c%22new%22%20york%20city%3e".getBytes();
+        byte[] bodyBytes = "foo=bar&a=b&empty=&city=%3c%22new%22%20york%20city%3e&other_city=los+angeles".getBytes();
         ByteBuf content = Unpooled.directBuffer(bodyBytes.length);
         content.writeBytes(bodyBytes);
 
@@ -838,7 +838,7 @@ public void testDecodeFullHttpRequestWithUrlEncodedBody() throws Exception {
         assertFalse(decoder.getBodyHttpDatas().isEmpty());
 
         assertFalse(decoder.getBodyHttpDatas().isEmpty());
-        assertEquals(4, decoder.getBodyHttpDatas().size());
+        assertEquals(5, decoder.getBodyHttpDatas().size());
 
         Attribute attr = (Attribute) decoder.getBodyHttpData("foo");
         assertTrue(attr.getByteBuf().isDirect());
@@ -856,6 +856,10 @@ public void testDecodeFullHttpRequestWithUrlEncodedBody() throws Exception {
         assertTrue(attr.getByteBuf().isDirect());
         assertEquals("<\"new\" york city>", attr.getValue());
 
+        attr = (Attribute) decoder.getBodyHttpData("other_city");
+        assertTrue(attr.getByteBuf().isDirect());
+        assertEquals("los angeles", attr.getValue());
+
         decoder.destroy();
         req.release();
     }
