Summary
Show content
Introduction
I’ve been migrating the HawAPI - A Free and Open Source API for Stranger Thingsproject from Spring Boot 2.7 to 3.3 and one intresting thing that I notice while running the tests is: Tests where the URL ends with a trailing slash (/) started failing with 404 status code.
Before Spring Boot 3.X (Spring Framework 6.X) the value of setUseTrailingSlashMatch
was always true
. However, with the latest major upgrade, this parameter has been deprecated and its value is now fixed at false
in all locations. (See commit b312eca)
According to the docs:
[…] support for trailing slashes is deprecated as of 6.0 in favor of configuring explicit redirects through a proxy, Servlet/web filter, or a controller. - PathPatternParser.java#L45-L61
Solutions for handle trailing slash
With that being said, let’s dive into our options:
Declaring multiple routes for every handler
The first solution is to explicit declare a second route mapping in all route handlers:
1@GetMapping({"/users/{uuid}", "/users/{uuid}/"})2public ResponseEntity<UserDTO> findUser(@PathVariable UUID uuid) {3 // ...4}5
6@PostMapping({"/users", "/users/"})7public ResponseEntity<UserDTO> registerUser(UserRegistrationDTO user) {8 // ...9}
The problem is: this solution will only work effectively if the application has a few number of mappings.
Using a custom OncePerRequestFilter
To avoid adding a second route mapping to all routes, we can create a custom OncePerRequestFilter
to redirect all request urls that contain trailing slash (/) to the url without it and using the 301 (Moved Permanently) status code.
1@Component2public class TrailingSlashHandlerFilter extends OncePerRequestFilter {3
4 @Override5 protected void doFilterInternal(6 HttpServletRequest request,7 HttpServletResponse response,8 FilterChain filterChain9 ) throws ServletException, IOException {10 String requestUri = request.getRequestURI();11
12 if (requestUri.endsWith("/")) {13 String newUrl = requestUri.substring(0, requestUri.length() - 1);14 response.setStatus(HttpStatus.MOVED_PERMANENTLY.value());15 response.setHeader(HttpHeaders.LOCATION, newUrl);16 return;17 }18
19 filterChain.doFilter(request, response);20 }21}
It’s a good option but we’ll need to redirect all requests the url without trailing slash, or vice versa.
Using the new UrlHandlerFilter
At the moment of writing this post, the version 6.2.0 wasn’t released yet. To be able to use the new UrlHandlerFilter
i’m using the version 6.2.0-M4. See how to install:
Overriding Spring Framework
- Add the milestone repository to your pom.xml file:
<repositories> <repository> <id>maven_central</id> <name>Maven Central</name> <url>https://repo.maven.apache.org/maven2/</url> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories>
- Override spring framework version:
1 <properties>2 <java.version>21</java.version>3 <spring-framework.version>6.2.0-M4</spring-framework.version>4 </properties>
In this version we can use the new UrlHandlerFilter with a few possible options:
- Redirect all or only specific urls with 301 (Moved Permanently)
- Handle all or only specific urls (Same as before Spring Boot 3.X (Spring Framework 6.X))
1@Component2public class TrailingSlashHandlerFilter extends OncePerRequestFilter {3
4 @Override5 protected void doFilterInternal(6 HttpServletRequest request,7 HttpServletResponse response,8 FilterChain filterChain9 ) throws ServletException, IOException {10 // Redirecting11 UrlHandlerFilter filter = UrlHandlerFilter.trimTrailingSlash("/**").andRedirect(HttpStatus.PERMANENT_REDIRECT).build();12
13 // Or transparently handle those for HTTP clients, without any redirect:14 UrlHandlerFilter filter = UrlHandlerFilter.trimTrailingSlash("/**").andHandleRequest().build();15 filter.doFilter(request, response, filterChain);16 }17}
The catch-all pattern (/**) matches any path, including nested paths.
- /api/test will - be handled.
- /api/test/subpath - will also be handled.
Conclusion
In this article, we discussed the deprecation of the trailing slash matching configuration option in Spring Boot 3 showing three possible ways, including the official UrlHandlerFilter filter, to fix this small but significant change.
According to @rstoyanchev, this deprecation will not be reverted:
[…] However, undeprecating trailing slash matching at this point, only to deprecate that later again, will only add to the confusion. - issuecomment-1748751881