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 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.
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:
Wrap example (mode ②) keeps the URL unchanged while the server processes the request without the trailing slash:
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
