Handling Trailing‑Slash URLs in Spring Boot 3 with UrlHandlerFilter

This article shows how Spring Boot 3.5.0’s built‑in UrlHandlerFilter can safely handle trailing‑slash URLs by either issuing a permanent redirect or wrapping the request, with step‑by‑step examples for Spring MVC, custom servlet filters, reactive WebFlux filters, and equivalent Nginx configuration.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Handling Trailing‑Slash URLs in Spring Boot 3 with UrlHandlerFilter

Spring MVC used to ignore a trailing slash when mapping requests, so a GET /home/ would be handled by a controller method annotated with @GetMapping("/home"). This behavior was deprecated in Spring 6.0 and removed in 7.0, leaving applications that still need to process such URLs safely.

UrlHandlerFilter Overview

The built‑in UrlHandlerFilter is designed to address this gap. It can be configured to either:

Return an HTTP redirect (e.g., 308 Permanent Redirect) when a request ends with a slash, guiding the browser to the slash‑less variant.

Wrap the request so that the downstream processing sees the URL without the trailing slash, without changing the client‑visible address.

Default Behavior Demonstration

With Spring Boot 3.5.0, the following controller shows the default handling of a trailing slash:

@RestController
@RequestMapping("/trailing")
public class TrailingSlashController {
    @GetMapping("/query")
    public ResponseEntity<?> query() {
        return ResponseEntity.ok("trailing slash query...");
    }
}

Accessing /trailing/query returns the expected response, while /trailing/query/ results in a 404 because the trailing slash is no longer ignored.

Result without trailing slash
Result without trailing slash
Result with trailing slash (404)
Result with trailing slash (404)

Configuring UrlHandlerFilter for MVC

Register the filter as a Spring bean:

@Configuration
public class TrailingSlashConfig {
    @Bean
    FilterRegistrationBean<UrlHandlerFilter> urlHandlerFilterRegistrationBean() {
        FilterRegistrationBean<UrlHandlerFilter> registrationBean = new FilterRegistrationBean<>();
        UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
            .trailingSlashHandler("/trailing/**")
            .redirect(HttpStatus.PERMANENT_REDIRECT) // ① redirect mode
            // .trailingSlashHandler("/trailing/**").wrapRequest() // ② wrap mode
            .build();
        registrationBean.setFilter(urlHandlerFilter);
        return registrationBean;
    }
}

Two modes are demonstrated:

Redirect mode – the filter sends a 308 redirect to the slash‑less URL. The browser’s address bar changes accordingly.

Wrap mode – the filter internally rewrites the request path; the client sees no address change.

Redirect example (mode ①) results in the following behavior:

Redirect to slash‑less URL
Redirect to slash‑less URL

Wrap example (mode ②) keeps the URL unchanged while the server processes the request without the trailing slash:

Internal request wrapping
Internal request wrapping

Alternative Interface Definition

If the filter is not used, developers can define both slash and non‑slash mappings manually:

@RestController
@RequestMapping("/home")
public class HomeController {
    @GetMapping("/query")
    public ResponseEntity<?> query() {
        return ResponseEntity.ok("home query...");
    }
    @GetMapping("/query/")
    public ResponseEntity<?> query2() {
        return query();
    }
}

Custom Servlet Filter for Redirect

A fully custom filter can detect a trailing slash, strip it, and forward the modified request:

public class TrailingSlashRedirectFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String path = httpRequest.getRequestURI();
        if (path.endsWith("/")) {
            String newPath = path.substring(0, path.length() - 1);
            HttpServletRequest newRequest = new CustomHttpServletRequestWrapper(httpRequest, newPath);
            chain.doFilter(newRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }
    private static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
        private final String newPath;
        public CustomHttpServletRequestWrapper(HttpServletRequest request, String newPath) {
            super(request);
            this.newPath = newPath;
        }
        @Override
        public String getRequestURI() { return newPath; }
        @Override
        public StringBuffer getRequestURL() {
            StringBuffer url = new StringBuffer();
            url.append(getScheme()).append("://").append(getServerName()).append(":" )
               .append(getServerPort()).append(newPath);
            return url;
        }
    }
}

Register it as a bean:

@Bean
FilterRegistrationBean<Filter> trailingSlashFilter() {
    FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new TrailingSlashRedirectFilter());
    registrationBean.addUrlPatterns("/*");
    return registrationBean;
}

Reactive WebFlux Filter

In a reactive stack, a WebFilter can perform the same rewrite:

@Component
public class TrailingSlashRedirectFilterReactive implements WebFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        if (path.endsWith("/")) {
            String newPath = path.substring(0, path.length() - 1);
            ServerHttpRequest newRequest = request.mutate().path(newPath).build();
            return chain.filter(exchange.mutate().request(newRequest).build());
        }
        return chain.filter(exchange);
    }
}

Since Spring 6.2, UrlHandlerFilter is also available for reactive applications:

UrlHandlerFilter filter = UrlHandlerFilter
    .trailingSlashHandler("/path1/**").redirect(HttpStatus.PERMANENT_REDIRECT)
    .trailingSlashHandler("/path2/**").mutateRequest()
    .build();

Nginx Proxy Configuration

When Spring Boot sits behind Nginx, a simple if block can strip trailing slashes before proxying:

location / {
    if ($request_uri ~ ^(.+)/$) {
        return 301 $1;
    }
    proxy_pass http://localhost:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

This configuration checks whether the request URI ends with a slash, captures the part before it, and issues a 301 redirect to the slash‑less version before forwarding the request to the Spring Boot application.

Overall, Spring Boot 3’s UrlHandlerFilter provides a concise, declarative way to handle trailing‑slash URLs across servlet and reactive stacks, while custom filters and Nginx rules offer flexible alternatives when finer‑grained control is required.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

spring-bootNginxWebFluxServlet FilterTrailing SlashUrlHandlerFilter
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.