Strip query string from Cloud Foundry reactive links base URL

CloudFoundryLinksHandler passes request.getURI().toString() directly
to EndpointLinksResolver.resolveLinks(), which includes the query
string. This causes generated link hrefs to contain the query
(for example, "/cfApplication?x=1/info"). The standard WebFlux and
Cloud Foundry servlet siblings already strip the query before
resolving links.

Use UriComponentsBuilder.replaceQuery(null) to match the pattern used
by WebFluxEndpointHandlerMapping and add a regression test for a
query-string request.

Signed-off-by: Lee JiWon <dlwldnjs1009@gmail.com>

See gh-50008
This commit is contained in:
Lee JiWon
2026-04-10 11:33:28 +01:00
committed by Andy Wilkinson
parent c9f9c45a2d
commit 847b2e837b
2 changed files with 24 additions and 1 deletions
@@ -49,6 +49,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available on
@@ -110,8 +111,9 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH
return new ResponseEntity<>(securityResponse.getStatus());
}
AccessLevel accessLevel = exchange.getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
String requestUri = UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null).toUriString();
Map<String, Link> links = CloudFoundryWebFluxEndpointHandlerMapping.this.linksResolver
.resolveLinks(request.getURI().toString());
.resolveLinks(requestUri);
return new ResponseEntity<>(
Collections.singletonMap("_links", getAccessibleLinks(accessLevel, links)), HttpStatus.OK);
});
@@ -61,6 +61,7 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.cors.CorsConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
@@ -166,6 +167,26 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
.isEqualTo(true)));
}
@Test
void linksToOtherEndpointsWithQueryStringShouldNotContainQueryInHref() {
given(this.tokenValidator.validate(any())).willReturn(Mono.empty());
given(this.securityService.getAccessLevel(any(), eq("app-id"))).willReturn(Mono.just(AccessLevel.FULL));
this.contextRunner.run(withWebTestClient((client) -> client.get()
.uri("/cfApplication?x=1")
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "bearer " + mockAccessToken())
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("_links.self.href")
.value((href) -> assertThat((String) href).doesNotContain("?").endsWith("/cfApplication"))
.jsonPath("_links.info.href")
.value((href) -> assertThat((String) href).doesNotContain("?").endsWith("/cfApplication/info"))
.jsonPath("_links.env.href")
.value((href) -> assertThat((String) href).doesNotContain("?").endsWith("/cfApplication/env"))));
}
@Test
void linksToOtherEndpointsForbidden() {
CloudFoundryAuthorizationException exception = new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,