Method level only, empty RequestMapping matches "" and "/"

Closes gh-30293
This commit is contained in:
rstoyanchev 2023-06-12 16:09:49 +01:00
parent 3fb98b6a97
commit bbab4faf7a
7 changed files with 53 additions and 15 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -48,15 +48,12 @@ public class DefaultControllerSpecTests {
.exchange() .exchange()
.expectStatus().isOk() .expectStatus().isOk()
.expectBody(String.class).isEqualTo("Success"); .expectBody(String.class).isEqualTo("Success");
}
@Test
public void controllerEmptyPath() {
new DefaultControllerSpec(new MyController()).build() new DefaultControllerSpec(new MyController()).build()
.get().uri("") .get().uri("")
.exchange() .exchange()
.expectStatus().isOk() .expectStatus().isOk()
.expectBody(String.class).isEqualTo("Success empty path"); .expectBody(String.class).isEqualTo("Success");
} }
@Test @Test
@ -121,19 +118,15 @@ public class DefaultControllerSpecTests {
} }
@SuppressWarnings("unused")
@RestController @RestController
private static class MyController { private static class MyController {
@GetMapping("/") @GetMapping
public String handleRootPath() { public String handleRootPath() {
return "Success"; return "Success";
} }
@GetMapping
public String handleEmptyPath() {
return "Success empty path";
}
@GetMapping("/exception") @GetMapping("/exception")
public void handleWithError() { public void handleWithError() {
throw new IllegalStateException(); throw new IllegalStateException();

View File

@ -89,8 +89,12 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
return " || "; return " || ";
} }
private boolean isEmptyPathMapping() { /**
return this.patterns == EMPTY_PATH_PATTERN; * Whether the condition is the "" (empty path) mapping.
* @since 6.0.10
*/
public boolean isEmptyPathMapping() {
return (this.patterns == EMPTY_PATH_PATTERN);
} }
/** /**

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -153,6 +153,9 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
if (typeInfo != null) { if (typeInfo != null) {
info = typeInfo.combine(info); info = typeInfo.combine(info);
} }
if (info.getPatternsCondition().isEmptyPathMapping()) {
info = info.mutate().paths("", "/").options(this.config).build();
}
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) { for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
if (entry.getValue().test(handlerType)) { if (entry.getValue().test(handlerType)) {
String prefix = entry.getKey(); String prefix = entry.getKey();

View File

@ -61,6 +61,17 @@ class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTe
} }
@ParameterizedHttpServerTest // gh-30293
void emptyMapping(HttpServer httpServer) throws Exception {
startServer(httpServer);
String url = "http://localhost:" + this.port;
assertThat(getRestTemplate().getForObject(url, String.class)).isEqualTo("root");
url += "/";
assertThat(getRestTemplate().getForObject(url, String.class)).isEqualTo("root");
}
@ParameterizedHttpServerTest @ParameterizedHttpServerTest
void httpHead(HttpServer httpServer) throws Exception { void httpHead(HttpServer httpServer) throws Exception {
startServer(httpServer); startServer(httpServer);
@ -106,6 +117,11 @@ class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTe
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static class TestRestController { private static class TestRestController {
@GetMapping
public String get() {
return "root";
}
@GetMapping("/text") @GetMapping("/text")
public String textGet() { public String textGet() {
return "Foo"; return "Foo";

View File

@ -255,6 +255,16 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
pprc.getPatternValues() : ((PatternsRequestCondition) condition).getPatterns()); pprc.getPatternValues() : ((PatternsRequestCondition) condition).getPatterns());
} }
/**
* Whether the request mapping has an empty URL path mapping.
* @since 6.0.10
*/
public boolean isEmptyMapping() {
RequestCondition<?> condition = getActivePatternsCondition();
return (condition instanceof PathPatternsRequestCondition pprc ?
pprc.isEmptyPathMapping() : ((PatternsRequestCondition) condition).isEmptyPathMapping());
}
/** /**
* Return the HTTP request methods of this {@link RequestMappingInfo}; * Return the HTTP request methods of this {@link RequestMappingInfo};
* or instance with 0 request methods (never {@code null}). * or instance with 0 request methods (never {@code null}).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -305,6 +305,9 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
if (typeInfo != null) { if (typeInfo != null) {
info = typeInfo.combine(info); info = typeInfo.combine(info);
} }
if (info.isEmptyMapping()) {
info = info.mutate().paths("", "/").options(this.config).build();
}
String prefix = getPathPrefix(handlerType); String prefix = getPathPrefix(handlerType);
if (prefix != null) { if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);

View File

@ -192,7 +192,16 @@ class ServletAnnotationControllerHandlerMethodTests extends AbstractServletHandl
request.setServletPath(""); request.setServletPath("");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response); getServlet().service(request, response);
assertThat(response.getContentAsString()).isEqualTo("test"); assertThat(response.getContentAsString()).isEqualTo("test");
// gh-30293
request = new MockHttpServletRequest("GET", "/");
response = new MockHttpServletResponse();
getServlet().service(request, response);
assertThat(response.getContentAsString()).isEqualTo("test");
} }
@PathPatternsParameterizedTest @PathPatternsParameterizedTest