Leading slash handling in UrlHandlerFilter

Closes gh-37030
This commit is contained in:
rstoyanchev
2026-08-14 09:11:50 +02:00
committed by Brian Clozel
parent 692dbc9160
commit 675f25de72
4 changed files with 30 additions and 15 deletions
@@ -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();
}
@@ -299,12 +299,14 @@ public final class UrlHandlerFilter implements WebFilter {
@Override
public Mono<Void> 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);
@@ -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();
}
@@ -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