From 30e3a5719e2ce270a8468a37e4879c170f60ca0d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 9 Jul 2026 13:00:48 +0100 Subject: [PATCH] Leading slash handling in UrlHandlerFilter Closes gh-37030 --- .../web/filter/UrlHandlerFilter.java | 3 +++ .../web/filter/reactive/UrlHandlerFilter.java | 6 ++++-- .../web/filter/UrlHandlerFilterTests.java | 15 ++++++++----- .../reactive/UrlHandlerFilterTests.java | 21 ++++++++++++------- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/UrlHandlerFilter.java b/spring-web/src/main/java/org/springframework/web/filter/UrlHandlerFilter.java index c3e99de04b7..18759fe694c 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/UrlHandlerFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/UrlHandlerFilter.java @@ -308,6 +308,9 @@ public final class UrlHandlerFilter extends OncePerRequestFilter { throws IOException { String location = trimTrailingSlash(request.getRequestURI()); + if (location.length() > 2 && location.startsWith("//")) { + location = (location.charAt(2) != '/' ? location.substring(1) : location); + } if (StringUtils.hasText(request.getQueryString())) { location += "?" + request.getQueryString(); } diff --git a/spring-web/src/main/java/org/springframework/web/filter/reactive/UrlHandlerFilter.java b/spring-web/src/main/java/org/springframework/web/filter/reactive/UrlHandlerFilter.java index 4afe02cdd64..057bd7bb188 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/reactive/UrlHandlerFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/reactive/UrlHandlerFilter.java @@ -299,12 +299,14 @@ public final class UrlHandlerFilter implements WebFilter { @Override public Mono handleInternal(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); - String query = request.getURI().getRawQuery(); String location = trimTrailingSlash(request); + if (location.length() > 2 && location.startsWith("//")) { + location = (location.charAt(2) != '/' ? location.substring(1) : location); + } + String query = request.getURI().getRawQuery(); if (StringUtils.hasText(query)) { location += "?" + query; } - ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(this.statusCode); response.getHeaders().set(HttpHeaders.LOCATION, location); diff --git a/spring-web/src/test/java/org/springframework/web/filter/UrlHandlerFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/UrlHandlerFilterTests.java index d8ed4ba81d9..07696b0944a 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/UrlHandlerFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/UrlHandlerFilterTests.java @@ -74,11 +74,16 @@ class UrlHandlerFilterTests { @Test void redirect() throws Exception { - HttpStatus status = HttpStatus.PERMANENT_REDIRECT; - UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").redirect(status).build(); + testRedirect("/**", "/path/123/", "/path/123"); + testRedirect("/**", "//path/123/", "/path/123"); + testRedirect("/**", "///path/123/", "///path/123"); + } - String path = "/path/123"; - MockHttpServletRequest request = new MockHttpServletRequest("GET", path + "/"); + private void testRedirect(String pattern, String path, String location) throws Exception { + HttpStatus status = HttpStatus.PERMANENT_REDIRECT; + UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler(pattern).redirect(status).build(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", path); MockHttpServletResponse response = new MockHttpServletResponse(); MockFilterChain chain = new MockFilterChain(); @@ -89,7 +94,7 @@ class UrlHandlerFilterTests { assertThat(chain.getRequest()).isNull(); assertThat(response.getStatus()).isEqualTo(status.value()); - assertThat(response.getHeader(HttpHeaders.LOCATION)).isEqualTo(path + "?" + queryString); + assertThat(response.getHeader(HttpHeaders.LOCATION)).isEqualTo(location + "?" + queryString); assertThat(response.isCommitted()).isTrue(); } diff --git a/spring-web/src/test/java/org/springframework/web/filter/reactive/UrlHandlerFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/reactive/UrlHandlerFilterTests.java index c3cc1e65554..32cef22215f 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/reactive/UrlHandlerFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/reactive/UrlHandlerFilterTests.java @@ -17,12 +17,14 @@ package org.springframework.web.filter.reactive; import java.net.URI; +import java.net.URISyntaxException; import java.util.List; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import reactor.core.publisher.Mono; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; @@ -59,20 +61,23 @@ class UrlHandlerFilterTests { } @Test - void redirect() { - HttpStatus status = HttpStatus.PERMANENT_REDIRECT; - UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").redirect(status).build(); + void redirect() throws URISyntaxException { + testRedirect("/**", new URI(null, null, "/path/123/", "foo=bar", null), "/path/123?foo=bar"); + // no way to create java.net.URI with leading slashes + } - String path = "/path/123"; - String queryString = "foo=bar"; - MockServerHttpRequest original = MockServerHttpRequest.get(path + "/?" + queryString).build(); - ServerWebExchange exchange = MockServerWebExchange.from(original); + private static void testRedirect(String pattern, URI uri, String location) { + HttpStatus status = HttpStatus.PERMANENT_REDIRECT; + UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler(pattern).redirect(status).build(); + + MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.GET, uri).build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); assertThatThrownBy(() -> invokeFilter(filter, exchange)) .hasMessageContaining("No argument value was captured"); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(status); - assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(URI.create(path + "?" + queryString)); + assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(URI.create(location)); } @Test