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);
}