From 01022fec6c39ba1703d8ebdd346adaa87c7807aa Mon Sep 17 00:00:00 2001 From: renechoi Date: Fri, 24 Jul 2026 13:38:34 +0900 Subject: [PATCH 1/3] Fix PathRoutePredicateFactory matchTrailingSlash with Spring Framework 7.0 Signed-off-by: renechoi --- .../handler/predicate/PathRoutePredicateFactory.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java index 1b67b65d9..32149eef0 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java @@ -110,17 +110,26 @@ public boolean test(ServerWebExchange exchange) { s -> parsePath(exchange.getRequest().getURI().getRawPath())); PathPattern match = null; + PathContainer pathForMatch = path; for (int i = 0; i < pathPatterns.size(); i++) { PathPattern pathPattern = pathPatterns.get(i); if (pathPattern.matches(path)) { match = pathPattern; break; } + else if (config.isMatchTrailingSlash() && path.value().endsWith("/")) { + PathContainer pathWithoutTrailingSlash = parsePath(path.value().substring(0, path.value().length() - 1)); + if (pathPattern.matches(pathWithoutTrailingSlash)) { + match = pathPattern; + pathForMatch = pathWithoutTrailingSlash; + break; + } + } } if (match != null) { traceMatch("Pattern", match.getPatternString(), path, true); - PathMatchInfo pathMatchInfo = match.matchAndExtract(path); + PathMatchInfo pathMatchInfo = match.matchAndExtract(pathForMatch); if (pathMatchInfo != null) { putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables()); } From f43f0a2e1dc149aa251d5f70e655dc8a0ba28d0c Mon Sep 17 00:00:00 2001 From: renechoi Date: Fri, 24 Jul 2026 13:40:23 +0900 Subject: [PATCH 2/3] Fix formatting Signed-off-by: renechoi --- .../gateway/handler/predicate/PathRoutePredicateFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java index 32149eef0..62a52b952 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java @@ -118,7 +118,8 @@ public boolean test(ServerWebExchange exchange) { break; } else if (config.isMatchTrailingSlash() && path.value().endsWith("/")) { - PathContainer pathWithoutTrailingSlash = parsePath(path.value().substring(0, path.value().length() - 1)); + PathContainer pathWithoutTrailingSlash = parsePath( + path.value().substring(0, path.value().length() - 1)); if (pathPattern.matches(pathWithoutTrailingSlash)) { match = pathPattern; pathForMatch = pathWithoutTrailingSlash; From 572cf2650828d70342e6f8fb53fd49ea89cb82ea Mon Sep 17 00:00:00 2001 From: kdelay Date: Fri, 24 Jul 2026 14:14:46 +0900 Subject: [PATCH 3/3] Add tests for matchTrailingSlash and remove stale FIXME comment Signed-off-by: kdelay --- .../predicate/PathRoutePredicateFactory.java | 2 - .../PathRoutePredicateFactoryTests.java | 45 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java index 62a52b952..e913594cb 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java @@ -86,8 +86,6 @@ public ShortcutType shortcutType() { public Predicate apply(Config config) { final ArrayList pathPatterns = new ArrayList<>(); synchronized (this.pathPatternParser) { - // FIXME: 5.0.0 setMatchOptionalTrailingSeparator missing - // pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchTrailingSlash()); config.getPatterns().forEach(pattern -> { String basePath = webFluxProperties.getBasePath(); boolean basePathIsNotBlank = StringUtils.hasText(basePath); diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java index 39313db5a..505c229d7 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java @@ -37,6 +37,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.security.web.server.firewall.StrictServerWebExchangeFirewall; import org.springframework.test.annotation.DirtiesContext; @@ -44,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.getUriTemplateVariables; @SpringBootTest(webEnvironment = RANDOM_PORT, properties = "debug=true") @DirtiesContext @@ -128,6 +131,48 @@ public void pathRouteWorksWithRegex() { .valueEquals(ROUTE_ID_HEADER, "path_regex"); } + @Test + public void trailingSlashMatchesWhenMatchTrailingSlashEnabled() { + Config config = new Config().setPatterns(Arrays.asList("/abc/{id}/function")).setMatchTrailingSlash(true); + Predicate predicate = new PathRoutePredicateFactory(new WebFluxProperties()).apply(config); + MockServerWebExchange exchange = MockServerWebExchange + .from(MockServerHttpRequest.get("/abc/123/function/").build()); + + assertThat(predicate.test(exchange)).isTrue(); + assertThat(getUriTemplateVariables(exchange)).containsEntry("id", "123"); + } + + @Test + public void trailingSlashDoesNotMatchWhenMatchTrailingSlashDisabled() { + Config config = new Config().setPatterns(Arrays.asList("/abc/{id}/function")).setMatchTrailingSlash(false); + Predicate predicate = new PathRoutePredicateFactory(new WebFluxProperties()).apply(config); + MockServerWebExchange exchange = MockServerWebExchange + .from(MockServerHttpRequest.get("/abc/123/function/").build()); + + assertThat(predicate.test(exchange)).isFalse(); + } + + @Test + public void pathWithoutTrailingSlashDoesNotMatchPatternWithTrailingSlash() { + Config config = new Config().setPatterns(Arrays.asList("/abc/123/function/")).setMatchTrailingSlash(true); + Predicate predicate = new PathRoutePredicateFactory(new WebFluxProperties()).apply(config); + MockServerWebExchange exchange = MockServerWebExchange + .from(MockServerHttpRequest.get("/abc/123/function").build()); + + assertThat(predicate.test(exchange)).isFalse(); + } + + @Test + public void patternWithTrailingSlashMatchesPathWithTrailingSlash() { + Config config = new Config().setPatterns(Arrays.asList("/abc/{id}/function/")).setMatchTrailingSlash(true); + Predicate predicate = new PathRoutePredicateFactory(new WebFluxProperties()).apply(config); + MockServerWebExchange exchange = MockServerWebExchange + .from(MockServerHttpRequest.get("/abc/123/function/").build()); + + assertThat(predicate.test(exchange)).isTrue(); + assertThat(getUriTemplateVariables(exchange)).containsEntry("id", "123"); + } + @Test public void matchOptionalTrailingSeparatorCopiedToMatchTrailingSlash() { Config config = new Config().setPatterns(Arrays.asList("patternA", "patternB")).setMatchTrailingSlash(false);