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 b173d9cb1c fix(request): preserve encoded paths when rebuilding the
request uri (#7685) (#7779)
b173d9cb1c is described below
commit b173d9cb1c7de71348c8f420ccd07789b9a62c88
Author: hanzhe-one <[email protected]>
AuthorDate: Mon Oct 19 09:28:06 2026 +0800
fix(request): preserve encoded paths when rebuilding the request uri
(#7685) (#7779)
* fix(request): preserve encoded paths when rebuilding the request uri
(#7685)
The request plugin parsed the already-encoded request URI into an
unencoded builder and called encode() again, so %2F in the path became
%252F even for header-only rules. This changed routing for escaped paths
as an unrelated side effect of header and cookie transformations.
Only rebuild the URI when query parameters change, and encode query names
and values once before building with build(true) so existing escapes are
preserved.
* test(request): cover remove and replace parameter keys (#7685)
Add regression coverage for the remaining parameter mutation paths:
both rebuild the URI while preserving the encoded path.
---
.../shenyu/plugin/request/RequestPlugin.java | 41 +++++++++---
.../shenyu/plugin/request/RequestPluginTest.java | 74 ++++++++++++++++++++++
2 files changed, 105 insertions(+), 10 deletions(-)
diff --git
a/shenyu-plugin/shenyu-plugin-request/src/main/java/org/apache/shenyu/plugin/request/RequestPlugin.java
b/shenyu-plugin/shenyu-plugin-request/src/main/java/org/apache/shenyu/plugin/request/RequestPlugin.java
index ee95120693..43edfb3348 100644
---
a/shenyu-plugin/shenyu-plugin-request/src/main/java/org/apache/shenyu/plugin/request/RequestPlugin.java
+++
b/shenyu-plugin/shenyu-plugin-request/src/main/java/org/apache/shenyu/plugin/request/RequestPlugin.java
@@ -39,8 +39,11 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
+import org.springframework.web.util.UriUtils;
import reactor.core.publisher.Mono;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -77,15 +80,13 @@ public class RequestPlugin extends AbstractShenyuPlugin {
}
ServerHttpRequest request = exchange.getRequest();
ServerWebExchange modifiedExchange = exchange.mutate()
- .request(originalRequest -> originalRequest.uri(
- UriComponentsBuilder.fromUri(exchange.getRequest()
- .getURI())
- .replaceQueryParams(getQueryParams(request,
requestHandle))
- .build()
- .encode()
- .toUri()
- ).headers(httpHeaders -> setHeaders(httpHeaders,
request, requestHandle))
- ).build();
+ .request(originalRequest -> {
+ originalRequest.headers(httpHeaders ->
setHeaders(httpHeaders, request, requestHandle));
+ if (Objects.nonNull(requestHandle.getParameter()) &&
requestHandle.getParameter().isNotEmptyConfig()) {
+ originalRequest.uri(buildRequestUri(request,
requestHandle));
+ }
+ })
+ .build();
return chain.execute(modifiedExchange);
}
@@ -103,7 +104,27 @@ public class RequestPlugin extends AbstractShenyuPlugin {
public boolean skip(final ServerWebExchange exchange) {
return skipExceptHttpLike(exchange);
}
-
+
+ /**
+ * Build the request uri while preserving the already encoded path.
+ * Query parameter names and values are encoded exactly once before the
uri is rebuilt.
+ *
+ * @param request serverHttpRequest
+ * @param requestHandle requestHandle
+ * @return the rebuilt request uri
+ */
+ private URI buildRequestUri(final ServerHttpRequest request, final
RequestHandle requestHandle) {
+ MultiValueMap<String, String> queryParams = getQueryParams(request,
requestHandle);
+ MultiValueMap<String, String> encodedQueryParams = new
LinkedMultiValueMap<>(queryParams.size());
+ queryParams.forEach((key, values) -> values.forEach(value ->
encodedQueryParams.add(
+ UriUtils.encodeQueryParam(key, StandardCharsets.UTF_8),
+ UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8))));
+ return UriComponentsBuilder.fromUri(request.getURI())
+ .replaceQueryParams(encodedQueryParams)
+ .build(true)
+ .toUri();
+ }
+
/**
* getHeaders.
*
diff --git
a/shenyu-plugin/shenyu-plugin-request/src/test/java/org/apache/shenyu/plugin/request/RequestPluginTest.java
b/shenyu-plugin/shenyu-plugin-request/src/test/java/org/apache/shenyu/plugin/request/RequestPluginTest.java
index 8608a17c4a..fc5092573e 100644
---
a/shenyu-plugin/shenyu-plugin-request/src/test/java/org/apache/shenyu/plugin/request/RequestPluginTest.java
+++
b/shenyu-plugin/shenyu-plugin-request/src/test/java/org/apache/shenyu/plugin/request/RequestPluginTest.java
@@ -45,6 +45,7 @@ import org.mockito.Mockito;
import org.mockito.internal.util.collections.Sets;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpCookie;
+import org.springframework.http.HttpMethod;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@@ -55,6 +56,7 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
+import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -257,6 +259,78 @@ public class RequestPluginTest {
})));
}
+ @Test
+ public void testHeaderOnlyRulePreservesEncodedPath() {
+ ruleData.setHandle("{\"header\":{\"setHeaders\":{\"X-Demo\":\"1\"}}}");
+ new RequestPluginHandler().handlerRule(ruleData);
+ ServerWebExchange encodedExchange =
MockServerWebExchange.from(MockServerHttpRequest
+ .method(HttpMethod.GET,
URI.create("http://localhost/files/a%2Fb?x=hello%20world"))
+ .build());
+ when(chain.execute(any())).thenReturn(Mono.empty());
+
+ StepVerifier.create(requestPlugin.doExecute(encodedExchange, chain,
new SelectorData(), ruleData)).verifyComplete();
+
+ ArgumentCaptor<ServerWebExchange> captor =
ArgumentCaptor.forClass(ServerWebExchange.class);
+ Mockito.verify(chain).execute(captor.capture());
+ assertEquals("/files/a%2Fb",
captor.getValue().getRequest().getURI().getRawPath());
+ assertEquals("x=hello%20world",
captor.getValue().getRequest().getURI().getRawQuery());
+ assertEquals("1",
captor.getValue().getRequest().getHeaders().getFirst("X-Demo"));
+ }
+
+ @Test
+ public void testParameterRulePreservesEncodedPath() {
+ ruleData.setHandle("{\"parameter\":{\"addParameters\":{\"added\":\"a
b\"}}}");
+ new RequestPluginHandler().handlerRule(ruleData);
+ ServerWebExchange encodedExchange =
MockServerWebExchange.from(MockServerHttpRequest
+ .method(HttpMethod.GET,
URI.create("http://localhost/files/a%2Fb?x=hello%20world"))
+ .build());
+ when(chain.execute(any())).thenReturn(Mono.empty());
+
+ StepVerifier.create(requestPlugin.doExecute(encodedExchange, chain,
new SelectorData(), ruleData)).verifyComplete();
+
+ ArgumentCaptor<ServerWebExchange> captor =
ArgumentCaptor.forClass(ServerWebExchange.class);
+ Mockito.verify(chain).execute(captor.capture());
+ URI requestUri = captor.getValue().getRequest().getURI();
+ assertEquals("/files/a%2Fb", requestUri.getRawPath());
+ assertEquals("x=hello%20world&added=a%20b", requestUri.getRawQuery());
+ }
+
+ @Test
+ public void testRemoveParameterRulePreservesEncodedPath() {
+
ruleData.setHandle("{\"parameter\":{\"removeParameterKeys\":[\"x\"]}}");
+ new RequestPluginHandler().handlerRule(ruleData);
+ ServerWebExchange encodedExchange =
MockServerWebExchange.from(MockServerHttpRequest
+ .method(HttpMethod.GET,
URI.create("http://localhost/files/a%2Fb?x=hello%20world&keep=1"))
+ .build());
+ when(chain.execute(any())).thenReturn(Mono.empty());
+
+ StepVerifier.create(requestPlugin.doExecute(encodedExchange, chain,
new SelectorData(), ruleData)).verifyComplete();
+
+ ArgumentCaptor<ServerWebExchange> captor =
ArgumentCaptor.forClass(ServerWebExchange.class);
+ Mockito.verify(chain).execute(captor.capture());
+ URI requestUri = captor.getValue().getRequest().getURI();
+ assertEquals("/files/a%2Fb", requestUri.getRawPath());
+ assertEquals("keep=1", requestUri.getRawQuery());
+ }
+
+ @Test
+ public void testReplaceParameterRulePreservesEncodedPath() {
+
ruleData.setHandle("{\"parameter\":{\"replaceParameterKeys\":{\"old\":\"new\"}}}");
+ new RequestPluginHandler().handlerRule(ruleData);
+ ServerWebExchange encodedExchange =
MockServerWebExchange.from(MockServerHttpRequest
+ .method(HttpMethod.GET,
URI.create("http://localhost/files/a%2Fb?old=1"))
+ .build());
+ when(chain.execute(any())).thenReturn(Mono.empty());
+
+ StepVerifier.create(requestPlugin.doExecute(encodedExchange, chain,
new SelectorData(), ruleData)).verifyComplete();
+
+ ArgumentCaptor<ServerWebExchange> captor =
ArgumentCaptor.forClass(ServerWebExchange.class);
+ Mockito.verify(chain).execute(captor.capture());
+ URI requestUri = captor.getValue().getRequest().getURI();
+ assertEquals("/files/a%2Fb", requestUri.getRawPath());
+ assertEquals("new=1", requestUri.getRawQuery());
+ }
+
@Test
public void testGetOrder() {
assertEquals(this.requestPlugin.getOrder(),
PluginEnum.REQUEST.getCode());