A Root basePath No Longer Creates a Double-Slash

Closes gh-17812

Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
This commit is contained in:
Andrey Litvitski 2025-09-03 22:07:07 +03:00 committed by Josh Cummings
parent 653f22d4a1
commit eca821471f
2 changed files with 14 additions and 3 deletions

View File

@ -49,6 +49,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
* </p> * </p>
* *
* @author Josh Cummings * @author Josh Cummings
* @author Andrey Litvitski
* @since 6.5 * @since 6.5
*/ */
public final class PathPatternRequestMatcher implements RequestMatcher { public final class PathPatternRequestMatcher implements RequestMatcher {
@ -200,14 +201,15 @@ public final class PathPatternRequestMatcher implements RequestMatcher {
* *
* <p> * <p>
* Prefixes should be of the form {@code /my/prefix}, starting with a slash, not * Prefixes should be of the form {@code /my/prefix}, starting with a slash, not
* ending in a slash, and not containing and wildcards * ending in a slash, and not containing and wildcards The special value
* {@code "/"} may be used to indicate the root context.
* @param basePath the path prefix * @param basePath the path prefix
* @return the {@link Builder} for more configuration * @return the {@link Builder} for more configuration
*/ */
public Builder basePath(String basePath) { public Builder basePath(String basePath) {
Assert.notNull(basePath, "basePath cannot be null"); Assert.notNull(basePath, "basePath cannot be null");
Assert.isTrue(basePath.startsWith("/"), "basePath must start with '/'"); Assert.isTrue(basePath.startsWith("/"), "basePath must start with '/'");
Assert.isTrue(!basePath.endsWith("/"), "basePath must not end with a slash"); Assert.isTrue("/".equals(basePath) || !basePath.endsWith("/"), "basePath must not end with a slash");
Assert.isTrue(!basePath.contains("*"), "basePath must not contain a star"); Assert.isTrue(!basePath.contains("*"), "basePath must not contain a star");
return new Builder(this.parser, basePath); return new Builder(this.parser, basePath);
} }
@ -282,7 +284,8 @@ public final class PathPatternRequestMatcher implements RequestMatcher {
public PathPatternRequestMatcher matcher(@Nullable HttpMethod method, String path) { public PathPatternRequestMatcher matcher(@Nullable HttpMethod method, String path) {
Assert.notNull(path, "pattern cannot be null"); Assert.notNull(path, "pattern cannot be null");
Assert.isTrue(path.startsWith("/"), "pattern must start with a /"); Assert.isTrue(path.startsWith("/"), "pattern must start with a /");
PathPattern pathPattern = this.parser.parse(this.basePath + path); String prefix = ("/".equals(this.basePath)) ? "" : this.basePath;
PathPattern pathPattern = this.parser.parse(prefix + path);
return new PathPatternRequestMatcher(pathPattern, return new PathPatternRequestMatcher(pathPattern,
(method != null) ? new HttpMethodRequestMatcher(method) : AnyRequestMatcher.INSTANCE); (method != null) ? new HttpMethodRequestMatcher(method) : AnyRequestMatcher.INSTANCE);
} }

View File

@ -137,6 +137,14 @@ public class PathPatternRequestMatcherTests {
.isThrownBy(() -> PathPatternRequestMatcher.withDefaults().basePath("/path/")); .isThrownBy(() -> PathPatternRequestMatcher.withDefaults().basePath("/path/"));
} }
@Test
void matcherWhenBasePathIsRootThenNoDoubleSlash() {
PathPatternRequestMatcher.Builder builder = PathPatternRequestMatcher.withDefaults().basePath("/");
RequestMatcher matcher = builder.matcher(HttpMethod.GET, "/path");
MockHttpServletRequest mock = get("/path").servletPath("/path").buildRequest(null);
assertThat(matcher.matches(mock)).isTrue();
}
MockHttpServletRequest request(String uri) { MockHttpServletRequest request(String uri) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
ServletRequestPathUtils.parseAndCache(request); ServletRequestPathUtils.parseAndCache(request);