Merge branch 'main' into pr/3761

This commit is contained in:
Ryan Baxter
2025-05-22 17:14:49 -04:00
19 changed files with 81 additions and 28 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
distribution: 'liberica'
java-version: '17'
cache: 'maven'
- name: Build with Maven

View File

@@ -5,7 +5,7 @@
To include Spring Cloud Gateway Server Web MVC in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-gateway-server-webmvc`.
See the https://projects.spring.io/spring-cloud/[Spring Cloud Project page] for details on setting up your build system with the current Spring Cloud Release Train.
If you include the starter, but you do not want the gateway to be enabled, set `spring.cloud.gateway.mvc.enabled=false`.
If you include the starter, but you do not want the gateway to be enabled, set `spring.cloud.gateway.server.webmvc.enabled=false`.
IMPORTANT: Spring Cloud Gateway Server MVC is built on https://spring.io/projects/spring-boot#learn[Spring Boot] and https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html[Spring WebMvc.fn].
As a consequence, many of the asynchronous or reactive libraries may not apply when you use Spring Cloud Gateway Server MVC.

View File

@@ -146,4 +146,19 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<!-- TODO: github actions fails with restricted header host -->
<jdk.httpclient.allowRestrictedHeaders>host</jdk.httpclient.allowRestrictedHeaders>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -22,11 +22,12 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(GatewayServerMvcAutoConfiguration.class)
@ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".enabled", matchIfMissing = true)
public class GatewayMvcClassPathWarningAutoConfiguration {
private static final Log log = LogFactory.getLog(GatewayMvcClassPathWarningAutoConfiguration.class);

View File

@@ -78,7 +78,7 @@ import org.springframework.web.client.RestClient;
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, RestTemplateAutoConfiguration.class,
RestClientAutoConfiguration.class, FilterAutoConfiguration.class, HandlerFunctionAutoConfiguration.class,
PredicateAutoConfiguration.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true)
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".enabled", matchIfMissing = true)
@Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class)
@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class)
public class GatewayServerMvcAutoConfiguration {

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())
@@ -351,7 +350,7 @@ public abstract class BeforeFilterFunctions {
queryParams.add(name, replacement);
}
MultiValueMap<String, String> encodedQueryParams = UriUtils.encodeQueryParams(queryParams);
MultiValueMap<String, String> encodedQueryParams = MvcUtils.encodeQueryParams(queryParams);
URI rewrittenUri = UriComponentsBuilder.fromUri(request.uri())
.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams))
.build(true)

View File

@@ -43,7 +43,7 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = { "spring.cloud.gateway.mvc.http-client.type=jdk" },
@SpringBootTest(properties = { "spring.http.client.factory=jdk" },
webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
public class VanillaRouterFunctionTests {

View File

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

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routesMap:
testsimplefunction:
uri: fn:upper

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routesMap:
route1:
uri: https://example1.com

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routesMap:
testsimplestream:
uri: stream:hello-out-0

View File

@@ -1,5 +1,5 @@
strip.prefix.static.uri: http://${httpbin.host}:${httpbin.port}
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routes:
- id: strip_prefix_static_port_config
uri: ${strip.prefix.static.uri}

View File

@@ -1,4 +1,4 @@
spring.cloud.gateway.mvc:
spring.cloud.gateway.server.webmvc:
routes:
- id: weight_high_test
uri: https://examplel1.com

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

@@ -1966,16 +1966,6 @@
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.mvc.routes",
"type": "java.util.List<org.springframework.cloud.gateway.route.RouteDefinition>",
"description": "List of Routes.",
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webflux.mvc.routes",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.observability.enabled",
"type": "java.lang.Boolean",

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()