Merge branch '4.1.x' into 4.2.x

This commit is contained in:
Ryan Baxter
2025-05-22 14:37:12 -04:00
8 changed files with 84 additions and 37 deletions

View File

@@ -45,7 +45,6 @@ import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
import org.springframework.web.util.UriUtils;
import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR;
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
@@ -216,7 +215,7 @@ public abstract class BeforeFilterFunctions {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(request.params());
queryParams.remove(name);
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
MultiValueMap<String, String> encodedQueryParams = MvcUtils.encodeQueryParams(queryParams);
// remove from uri
URI newUri = UriComponentsBuilder.fromUri(request.uri())

View File

@@ -119,6 +119,7 @@ class BeforeFilterFunctionsTests {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path")
.queryParam("foo", "bar")
.queryParam("baz[]", "qux[]")
.queryParam("quux", "corge+")
.buildRequest(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
@@ -127,7 +128,8 @@ class BeforeFilterFunctionsTests {
assertThat(result.param("foo")).isEmpty();
assertThat(result.param("baz[]")).isPresent().hasValue("qux[]");
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D");
assertThat(result.param("quux")).isPresent().hasValue("corge+");
assertThat(result.uri().toString()).hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D&quux=corge%2B");
}
@Test

View File

@@ -24,12 +24,12 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
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 static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
@@ -59,7 +59,8 @@ public class RemoveRequestParameterGatewayFilterFactory
queryParams.remove(config.getName());
try {
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
MultiValueMap<String, String> encodedQueryParams = ServerWebExchangeUtils
.encodeQueryParams(queryParams);
URI newUri = UriComponentsBuilder.fromUri(request.getURI())
.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
.build(true)

View File

@@ -24,13 +24,13 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
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 static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap;
@@ -71,7 +71,8 @@ public class RewriteRequestParameterGatewayFilterFactory
}
try {
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
MultiValueMap<String, String> encodedQueryParams = ServerWebExchangeUtils
.encodeQueryParams(queryParams);
URI uri = uriComponentsBuilder.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
.build(true)
.toUri();

View File

@@ -17,9 +17,11 @@
package org.springframework.cloud.gateway.support;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -48,9 +50,13 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* @author Spencer Gibb
@@ -260,6 +266,17 @@ public final class ServerWebExchangeUtils {
return encoded;
}
public static MultiValueMap<String, String> encodeQueryParams(MultiValueMap<String, String> params) {
MultiValueMap<String, String> encodedQueryParams = new LinkedMultiValueMap<>(params.size());
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
for (String value : entry.getValue()) {
encodedQueryParams.add(UriUtils.encode(entry.getKey(), StandardCharsets.UTF_8),
UriUtils.encode(value, StandardCharsets.UTF_8));
}
}
return CollectionUtils.unmodifiableMultiValueMap(encodedQueryParams);
}
public static HttpStatus parse(String statusString) {
HttpStatus httpStatus;

View File

@@ -49,8 +49,8 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
/**
* @author Peter Müller
*/
@SpringBootTest(properties = {"management.endpoint.gateway.enabled=true",
"management.endpoints.web.exposure.include=*", "spring.cloud.gateway.actuator.verbose.enabled=true"},
@SpringBootTest(properties = { "management.endpoint.gateway.enabled=true",
"management.endpoints.web.exposure.include=*", "spring.cloud.gateway.actuator.verbose.enabled=true" },
webEnvironment = RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@ActiveProfiles("redis-route-repository")
@@ -94,8 +94,9 @@ public class GatewayControllerEndpointRedisRefreshTest {
createOrUpdateRouteWithCors(cors);
Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertRouteHasCorsConfig(cors));
Awaitility.await().atMost(Duration.ofSeconds(3))
.untilAsserted(() -> assertPreflightAllowOrigin("http://example.org"));
Awaitility.await()
.atMost(Duration.ofSeconds(3))
.untilAsserted(() -> assertPreflightAllowOrigin("http://example.org"));
}
void createOrUpdateRouteWithCors(Map<String, Object> cors) {
@@ -108,41 +109,41 @@ public class GatewayControllerEndpointRedisRefreshTest {
testRouteDefinition.setMetadata(Map.of("cors", cors));
testClient.post()
.uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route")
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(testRouteDefinition))
.exchange()
.expectStatus()
.isCreated();
.uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route")
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(testRouteDefinition))
.exchange()
.expectStatus()
.isCreated();
testClient.post()
.uri("http://localhost:" + port + "/actuator/gateway/refresh")
.exchange()
.expectStatus()
.isOk();
.uri("http://localhost:" + port + "/actuator/gateway/refresh")
.exchange()
.expectStatus()
.isOk();
}
void assertRouteHasCorsConfig(Map<String, Object> cors) {
testClient.get()
.uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("$.metadata")
.value(map -> assertThat((Map<String, Object>) map).hasSize(1)
.containsEntry("cors", cors));
.uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("$.metadata")
.value(map -> assertThat((Map<String, Object>) map).hasSize(1).containsEntry("cors", cors));
}
void assertPreflightAllowOrigin(String origin) {
testClient.options()
.uri("http://localhost:" + port + "/")
.header("Origin", "http://example.org")
.header("Access-Control-Request-Method", "GET")
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.valueEquals("Access-Control-Allow-Origin", origin);
.uri("http://localhost:" + port + "/")
.header("Origin", "http://example.org")
.header("Access-Control-Request-Method", "GET")
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.valueEquals("Access-Control-Allow-Origin", origin);
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.cloud.gateway.filter.factory;
import java.net.URI;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -24,6 +26,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
@@ -123,6 +126,23 @@ class RemoveRequestParameterGatewayFilterFactoryTests {
assertThat(actualRequest.getQueryParams()).containsEntry("ccc", singletonList(",xyz"));
}
@Test
void removeRequestParameterFilterShouldHandleRemainingPlusSignParams() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, URI.create("http://localhost?foo=bar&aaa=%2Bxyz"))
.build();
exchange = MockServerWebExchange.from(request);
NameConfig config = new NameConfig();
config.setName("foo");
GatewayFilter filter = new RemoveRequestParameterGatewayFilterFactory().apply(config);
filter.filter(exchange, filterChain);
ServerHttpRequest actualRequest = captor.getValue().getRequest();
assertThat(actualRequest.getQueryParams()).doesNotContainKey("foo");
assertThat(actualRequest.getQueryParams()).containsEntry("aaa", singletonList("+xyz"));
}
@Test
void removeRequestParameterFilterShouldHandleEncodedParameterName() {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost")

View File

@@ -88,6 +88,12 @@ class RewriteRequestParameterGatewayFilterFactoryTests {
Map.of("campaign[]", List.of("blue"), "color", List.of("white")));
}
@Test
void rewriteRequestParameterFilterWithPlusSign() {
testRewriteRequestParameterFilter("color", "white+", "campaign=blue%2B&color=green",
Map.of("campaign", List.of("blue+"), "color", List.of("white+")));
}
private void testRewriteRequestParameterFilter(String name, String replacement, String query,
Map<String, List<String>> expectedQueryParams) {
GatewayFilter filter = new RewriteRequestParameterGatewayFilterFactory()