UrlHandlerFilter also removes query parameters and fragment when using HTTP redirect

Spring Module and Version

Spring Web – v7.0.1

Issue Description

When using the RedirectTrailingSlashHandler variant of the UrlHandlerFilter, the path portion of the request URI is stripped of it's trailing slash, and the result is used as the new Location header. The location becomes a relative URL to the original request.

E.g. for a request to https://www.example.com/foo/bar/////:

  • the path of the request URI will be /foo/bar/////
  • the Location header will be set to /foo/bar
  • the client will redirect to https://www.example.com/foo/bar

However for the same request with query parameters and/or a fragment, such as https://www.example.com/foo/bar/////?key=value#fragment:

  • the path of the request URI will still be /foo/bar/////
  • the Location header will again be set to /foo/bar
  • the client will redirect to https://www.example.com/foo/bar and the query parameters and fragment will become lost

Expected Behaviour

The redirect should remove the trailing slashes from the path only, and should not remove the query parameters and fragment from the original request, as they are important parts of the original request.

E.g. for https://www.example.com/foo/bar/////?key=value#fragment, the Location header should be either of the following:

  • /foo/bar?key=value#fragment (relative)
  • https://www.example.com/foo/bar?key=value#fragment (absolute)

Code Reference

This issue affects both the standard and reactive implementations of UrlHandlerFilter, so should be fixed for both classes:

  • Standard:

    @Override
    public void handleInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws IOException {
    response.resetBuffer();
    response.setStatus(this.httpStatus.value());
    response.setHeader(HttpHeaders.LOCATION, trimTrailingSlash(request.getRequestURI()));
    response.flushBuffer();
    }
  • Reactive:

    @Override
    public Mono<Void> handleInternal(ServerWebExchange exchange, WebFilterChain chain) {
    ServerHttpResponse response = exchange.getResponse();
    response.setStatusCode(this.statusCode);
    response.getHeaders().set(HttpHeaders.LOCATION, trimTrailingSlash(exchange.getRequest()));
    return Mono.empty();
    }

    As an aside, I think the reactive variant should return response.setComplete() instead of Mono.empty()?