Skip to content

Commit 3df77c6

Browse files
committed
Fix request mapping of endpoint path-mapped to /
Closes gh-35426
1 parent 31936f0 commit 3df77c6

File tree

10 files changed

+130
-16
lines changed

10 files changed

+130
-16
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -235,6 +235,24 @@ void toStringWhenIncludedExcludedEndpoints() {
235235
assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false");
236236
}
237237

238+
@Test
239+
void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() {
240+
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root");
241+
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("/", () -> List
242+
.of(mockEndpoint(EndpointId.of("root"), "/"), mockEndpoint(EndpointId.of("alpha"), "alpha"))));
243+
assertMatcher.doesNotMatch("/");
244+
assertMatcher.matches("/alpha");
245+
assertMatcher.matches("/alpha/sub");
246+
}
247+
248+
@Test
249+
void toEndpointWhenEndpointPathMappedToRootShouldMatchRoot() {
250+
ServerWebExchangeMatcher matcher = EndpointRequest.to("root");
251+
RequestMatcherAssert assertMatcher = assertMatcher(matcher,
252+
new PathMappedEndpoints("/", () -> List.of(mockEndpoint(EndpointId.of("root"), "/"))));
253+
assertMatcher.matches("/");
254+
}
255+
238256
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) {
239257
return assertMatcher(matcher, mockPathMappedEndpoints("/actuator"));
240258
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import org.junit.jupiter.api.Test;
2525

2626
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
27+
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.EndpointRequestMatcher;
2728
import org.springframework.boot.actuate.endpoint.EndpointId;
2829
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
2930
import org.springframework.boot.actuate.endpoint.Operation;
@@ -239,6 +240,24 @@ void toStringWhenIncludedExcludedEndpoints() {
239240
assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false");
240241
}
241242

243+
@Test
244+
void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() {
245+
EndpointRequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root");
246+
RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", () -> List
247+
.of(mockEndpoint(EndpointId.of("root"), "/"), mockEndpoint(EndpointId.of("alpha"), "alpha"))));
248+
assertMatcher.doesNotMatch("/");
249+
assertMatcher.matches("/alpha");
250+
assertMatcher.matches("/alpha/sub");
251+
}
252+
253+
@Test
254+
void toEndpointWhenEndpointPathMappedToRootShouldMatchRoot() {
255+
EndpointRequestMatcher matcher = EndpointRequest.to("root");
256+
RequestMatcherAssert assertMatcher = assertMatcher(matcher,
257+
new PathMappedEndpoints("", () -> List.of(mockEndpoint(EndpointId.of("root"), "/"))));
258+
assertMatcher.matches("/");
259+
}
260+
242261
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
243262
return assertMatcher(matcher, mockPathMappedEndpoints("/actuator"));
244263
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -140,7 +140,17 @@ public Iterator<PathMappedEndpoint> iterator() {
140140
}
141141

142142
private String getPath(PathMappedEndpoint endpoint) {
143-
return (endpoint != null) ? this.basePath + "/" + endpoint.getRootPath() : null;
143+
if (endpoint == null) {
144+
return null;
145+
}
146+
StringBuilder path = new StringBuilder(this.basePath);
147+
if (!this.basePath.equals("/")) {
148+
path.append("/");
149+
}
150+
if (!endpoint.getRootPath().equals("/")) {
151+
path.append(endpoint.getRootPath());
152+
}
153+
return path.toString();
144154
}
145155

146156
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -86,7 +86,7 @@ private String getPath(String rootPath, OperationParameter[] selectorParameters,
8686
boolean matchRemainingPathSegments) {
8787
StringBuilder path = new StringBuilder(rootPath);
8888
for (int i = 0; i < selectorParameters.length; i++) {
89-
path.append("/{");
89+
path.append((i != 0 || !rootPath.endsWith("/")) ? "/{" : "{");
9090
if (i == selectorParameters.length - 1 && matchRemainingPathSegments) {
9191
path.append("*");
9292
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,8 +19,10 @@
1919
import java.lang.reflect.Method;
2020
import java.nio.charset.StandardCharsets;
2121
import java.security.Principal;
22+
import java.util.ArrayList;
2223
import java.util.Collection;
2324
import java.util.LinkedHashMap;
25+
import java.util.List;
2426
import java.util.Map;
2527
import java.util.function.Supplier;
2628

@@ -176,10 +178,19 @@ protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint end
176178
private RequestMappingInfo createRequestMappingInfo(WebOperation operation) {
177179
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
178180
String path = this.endpointMapping.createSubPath(predicate.getPath());
181+
List<String> paths = new ArrayList<>();
182+
paths.add(path);
183+
if (!StringUtils.hasText(path)) {
184+
paths.add("/");
185+
}
179186
RequestMethod method = RequestMethod.valueOf(predicate.getHttpMethod().name());
180187
String[] consumes = StringUtils.toStringArray(predicate.getConsumes());
181188
String[] produces = StringUtils.toStringArray(predicate.getProduces());
182-
return RequestMappingInfo.paths(path).methods(method).consumes(consumes).produces(produces).build();
189+
return RequestMappingInfo.paths(paths.toArray(new String[0]))
190+
.methods(method)
191+
.consumes(consumes)
192+
.produces(produces)
193+
.build();
183194
}
184195

185196
private void registerLinksMapping() {

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,13 @@ protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpo
196196
}
197197

198198
private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
199-
return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path))
199+
String subPath = this.endpointMapping.createSubPath(path);
200+
List<String> paths = new ArrayList<>();
201+
paths.add(subPath);
202+
if (!StringUtils.hasLength(subPath)) {
203+
paths.add("/");
204+
}
205+
return RequestMappingInfo.paths(paths.toArray(new String[0]))
200206
.options(this.builderConfig)
201207
.methods(RequestMethod.valueOf(predicate.getHttpMethod().name()))
202208
.consumes(predicate.getConsumes().toArray(new String[0]))

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -91,6 +91,20 @@ void getPathWhenMissingIdShouldReturnNull() {
9191
assertThat(mapped.getPath(EndpointId.of("xx"))).isNull();
9292
}
9393

94+
@Test
95+
void getPathWhenBasePathIsRootAndEndpointIsPathMappedToRootShouldReturnSingleSlash() {
96+
PathMappedEndpoints mapped = new PathMappedEndpoints("/",
97+
() -> List.of(mockEndpoint(EndpointId.of("root"), "/")));
98+
assertThat(mapped.getPath(EndpointId.of("root"))).isEqualTo("/");
99+
}
100+
101+
@Test
102+
void getPathWhenBasePathIsRootAndEndpointIsPathMapped() {
103+
PathMappedEndpoints mapped = new PathMappedEndpoints("/",
104+
() -> List.of(mockEndpoint(EndpointId.of("a"), "alpha")));
105+
assertThat(mapped.getPath(EndpointId.of("a"))).isEqualTo("/alpha");
106+
}
107+
94108
@Test
95109
void getAllRootPathsShouldReturnAllPaths() {
96110
PathMappedEndpoints mapped = createTestMapped(null);

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
import org.springframework.boot.actuate.endpoint.annotation.Selector;
3939
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
4040
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
41+
import org.springframework.boot.actuate.endpoint.web.PathMapper;
4142
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
4243
import org.springframework.context.ApplicationContext;
4344
import org.springframework.context.ConfigurableApplicationContext;
@@ -108,6 +109,21 @@ void readOperationWithEndpointsMappedToTheRoot() {
108109
.isEqualTo(true));
109110
}
110111

112+
@Test
113+
void readOperationWithEndpointPathMappedToTheRoot() {
114+
load(EndpointPathMappedToRootConfiguration.class, "", (client) -> {
115+
client.get().uri("/").exchange().expectStatus().isOk().expectBody().jsonPath("All").isEqualTo(true);
116+
client.get()
117+
.uri("/some-part")
118+
.exchange()
119+
.expectStatus()
120+
.isOk()
121+
.expectBody()
122+
.jsonPath("part")
123+
.isEqualTo("some-part");
124+
});
125+
}
126+
111127
@Test
112128
void readOperationWithSelector() {
113129
load(TestEndpointConfiguration.class,
@@ -672,6 +688,17 @@ public TestEndpoint testEndpoint(EndpointDelegate endpointDelegate) {
672688

673689
}
674690

691+
@Configuration(proxyBeanMethods = false)
692+
@Import(TestEndpointConfiguration.class)
693+
protected static class EndpointPathMappedToRootConfiguration {
694+
695+
@Bean
696+
PathMapper pathMapper() {
697+
return (endpointId) -> "/";
698+
}
699+
700+
}
701+
675702
@Configuration(proxyBeanMethods = false)
676703
@Import(BaseConfiguration.class)
677704
static class MatchAllRemainingEndpointConfiguration {

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,9 +20,11 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222

23+
import org.springframework.beans.factory.ObjectProvider;
2324
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
2425
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
2526
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
27+
import org.springframework.boot.actuate.endpoint.web.PathMapper;
2628
import org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader;
2729
import org.springframework.context.ApplicationContext;
2830
import org.springframework.context.annotation.Bean;
@@ -62,11 +64,11 @@ EndpointMediaTypes endpointMediaTypes() {
6264

6365
@Bean
6466
WebEndpointDiscoverer webEndpointDiscoverer(EndpointMediaTypes endpointMediaTypes,
65-
ApplicationContext applicationContext) {
67+
ApplicationContext applicationContext, ObjectProvider<PathMapper> pathMappers) {
6668
ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper(
6769
DefaultConversionService.getSharedInstance());
68-
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null,
69-
Collections.emptyList(), Collections.emptyList());
70+
return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes,
71+
pathMappers.orderedStream().toList(), Collections.emptyList(), Collections.emptyList());
7072
}
7173

7274
@Bean

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -68,6 +68,13 @@ void getRequestPredicateReturnsPredicateWithPath() {
6868
assertThat(requestPredicate.getPath()).isEqualTo("/root/{one}/{*two}");
6969
}
7070

71+
@Test
72+
void getRequestPredicateWithSlashRootReturnsPredicateWithPathWithoutDoubleSlash() {
73+
DiscoveredOperationMethod operationMethod = getDiscoveredOperationMethod(ValidSelectors.class);
74+
WebOperationRequestPredicate requestPredicate = this.factory.getRequestPredicate("/", operationMethod);
75+
assertThat(requestPredicate.getPath()).isEqualTo("/{one}/{*two}");
76+
}
77+
7178
private DiscoveredOperationMethod getDiscoveredOperationMethod(Class<?> source) {
7279
Method method = source.getDeclaredMethods()[0];
7380
AnnotationAttributes attributes = new AnnotationAttributes();

0 commit comments

Comments
 (0)