This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 0cddf52c07 fix(param-mapping): preserve valueless form parameters 
(#7902)
0cddf52c07 is described below

commit 0cddf52c07208ccefbfe07b6a0aa28c61c2a729f
Author: cloudbase-ai <[email protected]>
AuthorDate: Wed Oct 21 14:16:44 2026 +0800

    fix(param-mapping): preserve valueless form parameters (#7902)
    
    Signed-off-by: cloudbase-ai <[email protected]>
    Co-authored-by: cloudbase-ai 
<[email protected]>
---
 .../param/mapping/strategy/FormDataOperator.java   | 32 ++-----------
 .../mapping/strategy/FormDataOperatorTest.java     | 55 +++++++++++++++++++++-
 2 files changed, 59 insertions(+), 28 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-param-mapping/src/main/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperator.java
 
b/shenyu-plugin/shenyu-plugin-param-mapping/src/main/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperator.java
index 5dbc22c7ea..60332f8f6d 100644
--- 
a/shenyu-plugin/shenyu-plugin-param-mapping/src/main/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperator.java
+++ 
b/shenyu-plugin/shenyu-plugin-param-mapping/src/main/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperator.java
@@ -17,11 +17,11 @@
 
 package org.apache.shenyu.plugin.param.mapping.strategy;
 
+import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import com.jayway.jsonpath.DocumentContext;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.shenyu.common.dto.convert.rule.impl.ParamMappingRuleHandle;
-import org.apache.shenyu.common.exception.ShenyuException;
 import org.apache.shenyu.common.utils.GsonUtils;
 import org.apache.shenyu.plugin.api.ShenyuPluginChain;
 import org.apache.shenyu.plugin.base.support.BodyInserterContext;
@@ -41,13 +41,7 @@ import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
 import java.util.Objects;
 import java.util.function.Function;
 
@@ -58,6 +52,9 @@ public class FormDataOperator implements Operator {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(FormDataOperator.class);
 
+    // GsonUtils normalizes null strings to empty strings; form values must 
retain that distinction.
+    private static final Gson FORM_DATA_GSON = new Gson();
+
     @Override
     public Mono<Void> apply(final ServerWebExchange exchange, final 
ShenyuPluginChain shenyuPluginChain,
             final ParamMappingRuleHandle paramMappingRuleHandle) {
@@ -76,18 +73,11 @@ public class FormDataOperator implements Operator {
                     }
                     HttpHeaders headers = exchange.getRequest().getHeaders();
                     HttpHeaders httpHeaders = new HttpHeaders();
-                    Charset charset = 
Objects.requireNonNull(headers.getContentType()).getCharset();
-                    charset = Objects.isNull(charset) ? StandardCharsets.UTF_8 
: charset;
                     LinkedMultiValueMap<String, String> modifyMap = 
toLinkedMultiValueMap(modify);
-                    List<String> list = prepareParams(modifyMap, 
charset.name());
-                    String content = String.join("&", list);
-                    byte[] bodyBytes = content.getBytes(charset);
-                    int contentLength = bodyBytes.length;
                     final BodyInserter<LinkedMultiValueMap<String, String>, 
ReactiveHttpOutputMessage> bodyInserter = BodyInserters
                             .fromValue(modifyMap);
                     httpHeaders.putAll(headers);
                     httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
-                    httpHeaders.setContentLength(contentLength);
                     CachedBodyOutputMessage cachedBodyOutputMessage = new 
CachedBodyOutputMessage(exchange,
                             httpHeaders);
                     return bodyInserter.insert(cachedBodyOutputMessage, new 
BodyInserterContext())
@@ -109,18 +99,6 @@ public class FormDataOperator implements Operator {
         }
     }
 
-    private List<String> prepareParams(final LinkedMultiValueMap<String, 
String> modifyMap, final String charset) {
-        List<String> paramList = new ArrayList<>();
-        modifyMap.forEach((K, V) -> V.forEach(value -> {
-            try {
-                paramList.add(String.join("=", K, URLEncoder.encode(value, 
charset)));
-            } catch (UnsupportedEncodingException e) {
-                throw new ShenyuException(e);
-            }
-        }));
-        return paramList;
-    }
-
     /**
      * To linked multiValue map.
      *
@@ -128,7 +106,7 @@ public class FormDataOperator implements Operator {
      * @return the linked multiValue map
      */
     public LinkedMultiValueMap<String, String> toLinkedMultiValueMap(final 
String json) {
-        return GsonUtils.getGson().fromJson(json, new 
TypeToken<LinkedMultiValueMap<String, String>>() {
+        return FORM_DATA_GSON.fromJson(json, new 
TypeToken<LinkedMultiValueMap<String, String>>() {
         }.getType());
     }
 
diff --git 
a/shenyu-plugin/shenyu-plugin-param-mapping/src/test/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperatorTest.java
 
b/shenyu-plugin/shenyu-plugin-param-mapping/src/test/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperatorTest.java
index b9ea21f7a8..fa612c176f 100644
--- 
a/shenyu-plugin/shenyu-plugin-param-mapping/src/test/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperatorTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-param-mapping/src/test/java/org/apache/shenyu/plugin/param/mapping/strategy/FormDataOperatorTest.java
@@ -26,20 +26,24 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.EmptySource;
 import org.junit.jupiter.params.provider.NullSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
 import org.springframework.mock.web.server.MockServerWebExchange;
 import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
 import java.net.URLEncoder;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.HashSet;
@@ -134,6 +138,51 @@ public class FormDataOperatorTest {
         verify(this.chain).execute(formExchange);
     }
 
+    @ParameterizedTest
+    @CsvSource({
+        "flag,flag&injected=yes",
+        "flag=,flag=&injected=yes",
+        "flag&flag=&flag=value,flag&flag=&flag=value&injected=yes",
+        
"encoded%20key&name=hello+world,encoded+key&name=hello+world&injected=yes"
+    })
+    public void testPreserveValuelessAndEmptyFormParameters(final String 
originalBody, final String expectedBody) {
+        ServerWebExchange formExchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/form")
+                
.contentType(MediaType.APPLICATION_FORM_URLENCODED).body(originalBody));
+
+        String body = applyAndReadBody(formExchange, additionRule("yes"));
+
+        assertEquals(expectedBody, body);
+        ArgumentCaptor<ServerWebExchange> downstream = 
ArgumentCaptor.forClass(ServerWebExchange.class);
+        verify(this.chain).execute(downstream.capture());
+        assertEquals(body.getBytes(StandardCharsets.UTF_8).length, 
downstream.getValue().getRequest().getHeaders().getContentLength());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"UTF-8", "ISO-8859-1", "UTF-16LE"})
+    public void testFormEncodingAndLengthUseTheConfiguredCharset(final String 
charsetName) {
+        Charset charset = Charset.forName(charsetName);
+        String originalBody = URLEncoder.encode("clé", charset) + "=" + 
URLEncoder.encode("été &+", charset) + "&flag";
+        ServerWebExchange formExchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/form")
+                .contentType(new MediaType("application", 
"x-www-form-urlencoded", charset))
+                .body(Flux.just(new 
DefaultDataBufferFactory().wrap(originalBody.getBytes(charset)))));
+
+        String body = applyAndReadBody(formExchange, additionRule("yes"), 
charset);
+
+        assertEquals(originalBody + "&injected=yes", body);
+        ArgumentCaptor<ServerWebExchange> downstream = 
ArgumentCaptor.forClass(ServerWebExchange.class);
+        verify(this.chain).execute(downstream.capture());
+        assertEquals(charset, 
downstream.getValue().getRequest().getHeaders().getContentType().getCharset());
+        assertEquals(body.getBytes(charset).length, 
downstream.getValue().getRequest().getHeaders().getContentLength());
+    }
+
+    @Test
+    public void testAddValuelessParameter() {
+        ServerWebExchange formExchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/form")
+                
.contentType(MediaType.APPLICATION_FORM_URLENCODED).body("a=1"));
+
+        assertEquals("a=1&injected", applyAndReadBody(formExchange, 
additionRule(null)));
+    }
+
     private ParamMappingRuleHandle additionRule(final String value) {
         ParamMappingRuleHandle.ParamMapInfo add = new 
ParamMappingRuleHandle.ParamMapInfo();
         add.setPath("$");
@@ -145,12 +194,16 @@ public class FormDataOperatorTest {
     }
 
     private String applyAndReadBody(final ServerWebExchange formExchange, 
final ParamMappingRuleHandle rule) {
+        return applyAndReadBody(formExchange, rule, StandardCharsets.UTF_8);
+    }
+
+    private String applyAndReadBody(final ServerWebExchange formExchange, 
final ParamMappingRuleHandle rule, final Charset charset) {
         AtomicReference<String> body = new AtomicReference<>("");
         when(this.chain.execute(any())).thenAnswer(invocation -> {
             ServerWebExchange downstream = invocation.getArgument(0);
             return 
DataBufferUtils.join(downstream.getRequest().getBody()).doOnNext(buffer -> {
                 try {
-                    body.set(buffer.toString(StandardCharsets.UTF_8));
+                    body.set(buffer.toString(charset));
                 } finally {
                     DataBufferUtils.release(buffer);
                 }

Reply via email to