Fix EndpointRequest links matching for separate management port

When management.endpoints.web.base-path is empty and management runs
on a different port, EndpointRequest.toLinks() and toAnyEndpoint()
do not match the links endpoint consistently.

Derive the links path based on the management port type on both
servlet and reactive sides, and add regression tests for each
implementation.

See gh-49591

Signed-off-by: bbbbooo <hyeons1213@gmail.com>
This commit is contained in:
bbbbooo
2026-03-16 16:39:12 +00:00
committed by Andy Wilkinson
parent ab7d6a9d29
commit 79f91eac56
4 changed files with 117 additions and 17 deletions
@@ -228,6 +228,13 @@ public final class EndpointRequest {
&& applicationContext.getParent() == null;
}
protected final String getLinksPath(String basePath) {
if (StringUtils.hasText(basePath)) {
return basePath;
}
return (this.managementPortType == ManagementPortType.DIFFERENT) ? "/" : null;
}
protected final String toString(List<Object> endpoints, String emptyValue) {
return (!endpoints.isEmpty()) ? endpoints.stream()
.map(this::getEndpointId)
@@ -326,7 +333,8 @@ public final class EndpointRequest {
streamPaths(this.includes, endpoints).forEach(paths::add);
streamPaths(this.excludes, endpoints).forEach(paths::remove);
List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths, this.httpMethod);
if (this.includeLinks && StringUtils.hasText(endpoints.getBasePath())) {
String linksPath = getLinksPath(endpoints.getBasePath());
if (this.includeLinks && linksPath != null) {
delegateMatchers.add(new LinksServerWebExchangeMatcher());
}
if (delegateMatchers.isEmpty()) {
@@ -362,10 +370,17 @@ public final class EndpointRequest {
@Override
protected ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties) {
if (StringUtils.hasText(properties.getBasePath())) {
return new OrServerWebExchangeMatcher(
new PathPatternParserServerWebExchangeMatcher(properties.getBasePath()),
new PathPatternParserServerWebExchangeMatcher(properties.getBasePath() + "/"));
String linksPath = getLinksPath(properties.getBasePath());
if (linksPath != null) {
List<ServerWebExchangeMatcher> linksMatchers = new ArrayList<>();
linksMatchers.add(new PathPatternParserServerWebExchangeMatcher(linksPath));
if ("/".equals(linksPath)) {
linksMatchers.add(new PathPatternParserServerWebExchangeMatcher(""));
}
else {
linksMatchers.add(new PathPatternParserServerWebExchangeMatcher(linksPath + "/"));
}
return new OrServerWebExchangeMatcher(linksMatchers);
}
return EMPTY_MATCHER;
}
@@ -225,13 +225,27 @@ public final class EndpointRequest {
}
protected List<RequestMatcher> getLinksMatchers(RequestMatcherFactory requestMatcherFactory,
RequestMatcherProvider matcherProvider, String basePath) {
RequestMatcherProvider matcherProvider, String linksPath) {
List<RequestMatcher> linksMatchers = new ArrayList<>();
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, basePath));
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, basePath, "/"));
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, linksPath));
if (!"/".equals(linksPath)) {
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, linksPath, "/"));
}
return linksMatchers;
}
protected String getLinksPath(WebApplicationContext context, String basePath) {
if (StringUtils.hasText(basePath)) {
return basePath;
}
ManagementPortType managementPortType = this.managementPortType;
if (managementPortType == null) {
managementPortType = ManagementPortType.get(context.getEnvironment());
this.managementPortType = managementPortType;
}
return (managementPortType == ManagementPortType.DIFFERENT) ? "/" : null;
}
protected RequestMatcherProvider getRequestMatcherProvider(WebApplicationContext context) {
try {
return getRequestMatcherProviderBean(context);
@@ -359,8 +373,9 @@ public final class EndpointRequest {
List<RequestMatcher> delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths,
this.httpMethod);
String basePath = endpoints.getBasePath();
if (this.includeLinks && StringUtils.hasText(basePath)) {
delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory, matcherProvider, basePath));
String linksPath = getLinksPath(context, basePath);
if (this.includeLinks && linksPath != null) {
delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory, matcherProvider, linksPath));
}
if (delegateMatchers.isEmpty()) {
return EMPTY_MATCHER;
@@ -393,10 +408,10 @@ public final class EndpointRequest {
protected RequestMatcher createDelegate(WebApplicationContext context,
RequestMatcherFactory requestMatcherFactory) {
WebEndpointProperties properties = context.getBean(WebEndpointProperties.class);
String basePath = properties.getBasePath();
if (StringUtils.hasText(basePath)) {
String linksPath = getLinksPath(context, properties.getBasePath());
if (linksPath != null) {
return new OrRequestMatcher(
getLinksMatchers(requestMatcherFactory, getRequestMatcherProvider(context), basePath));
getLinksMatchers(requestMatcherFactory, getRequestMatcherProvider(context), linksPath));
}
return EMPTY_MATCHER;
}
@@ -20,6 +20,7 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.AssertDelegateTarget;
import org.junit.jupiter.api.Test;
@@ -35,6 +36,7 @@ import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.web.context.WebServerApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
@@ -91,6 +93,15 @@ class EndpointRequestTests {
assertMatcher.matches("/bar");
}
@Test
void toAnyEndpointWhenBasePathIsEmptyAndManagementPortDifferentShouldMatchLinks() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint();
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""),
WebServerNamespace.MANAGEMENT, true);
assertMatcher.matches("/");
assertMatcher.matches("/foo");
}
@Test
void toAnyEndpointShouldNotMatchOtherPath() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint();
@@ -143,6 +154,15 @@ class EndpointRequestTests {
assertMatcher.doesNotMatch("/");
}
@Test
void toLinksWhenBasePathEmptyAndManagementPortDifferentShouldMatchRoot() {
ServerWebExchangeMatcher matcher = EndpointRequest.toLinks();
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""),
WebServerNamespace.MANAGEMENT, true);
assertMatcher.matches("/");
assertMatcher.doesNotMatch("/foo");
}
@Test
void excludeByClassShouldNotMatchExcluded() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint()
@@ -325,10 +345,25 @@ class EndpointRequestTests {
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher,
PathMappedEndpoints pathMappedEndpoints, WebServerNamespace namespace) {
return assertMatcher(matcher, pathMappedEndpoints, namespace, false);
}
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher,
PathMappedEndpoints pathMappedEndpoints, WebServerNamespace namespace, boolean managementPortDifferent) {
StaticApplicationContext context = new StaticApplicationContext();
if (namespace != null && !WebServerNamespace.SERVER.equals(namespace)) {
NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace);
context.setParent(parentContext);
if (managementPortDifferent) {
context = new NamedStaticWebApplicationContext(namespace);
}
else {
NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace);
context.setParent(parentContext);
}
}
if (managementPortDifferent) {
context.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("test", Map.of("management.server.port", 0)));
}
context.registerBean(WebEndpointProperties.class);
if (pathMappedEndpoints != null) {
@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.autoconfigure.security.servlet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.assertj.core.api.AssertDelegateTarget;
@@ -36,6 +37,7 @@ import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.web.context.WebServerApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
@@ -91,6 +93,15 @@ class EndpointRequestTests {
assertMatcher.matches("/bar");
}
@Test
void toAnyEndpointWhenBasePathIsEmptyAndManagementPortDifferentShouldMatchLinks() {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""), null,
WebServerNamespace.MANAGEMENT, true);
assertMatcher.matches("/");
assertMatcher.matches("/foo");
}
@Test
void toAnyEndpointShouldNotMatchOtherPath() {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
@@ -150,6 +161,15 @@ class EndpointRequestTests {
assertMatcher.doesNotMatch("/");
}
@Test
void toLinksWhenBasePathEmptyAndManagementPortDifferentShouldMatchRoot() {
RequestMatcher matcher = EndpointRequest.toLinks();
RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""), null,
WebServerNamespace.MANAGEMENT, true);
assertMatcher.matches("/");
assertMatcher.doesNotMatch("/foo");
}
@Test
void excludeByClassShouldNotMatchExcluded() {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding(FooEndpoint.class, BazServletEndpoint.class);
@@ -350,10 +370,25 @@ class EndpointRequestTests {
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEndpoints pathMappedEndpoints,
RequestMatcherProvider matcherProvider, WebServerNamespace namespace) {
return assertMatcher(matcher, pathMappedEndpoints, matcherProvider, namespace, false);
}
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEndpoints pathMappedEndpoints,
RequestMatcherProvider matcherProvider, WebServerNamespace namespace, boolean managementPortDifferent) {
StaticWebApplicationContext context = new StaticWebApplicationContext();
if (namespace != null && !WebServerNamespace.SERVER.equals(namespace)) {
NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace);
context.setParent(parentContext);
if (managementPortDifferent) {
context = new NamedStaticWebApplicationContext(namespace);
}
else {
NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace);
context.setParent(parentContext);
}
}
if (managementPortDifferent) {
context.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("test", Map.of("management.server.port", 0)));
}
context.registerBean(WebEndpointProperties.class);
if (pathMappedEndpoints != null) {