I have traced the issue to the following commit: https://github.com/spring-projects/spring-framework/commit/d927d64c40decba86e1ec44cedda7ddecf9c0aad
Reproducer:
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import reactor.core.publisher.Mono;
import java.util.Map;
@ExtendWith(MockitoExtension.class)
class HttpRequestValuesTest {
@Mock private ExchangeFunction exchangeFunction;
@Captor private ArgumentCaptor<ClientRequest> captor;
@HttpExchange(
url = "/reactive/v1",
accept = MediaType.APPLICATION_JSON_VALUE,
contentType = MediaType.APPLICATION_JSON_VALUE)
interface TestClient {
@GetExchange
String get(@RequestParam Map<String, String> queryParams);
}
@Test
void reproducer() {
ClientResponse mockResponse = mock();
when(mockResponse.statusCode()).thenReturn(HttpStatus.OK);
when(mockResponse.bodyToMono(Void.class)).thenReturn(Mono.empty());
given(exchangeFunction.exchange(captor.capture())).willReturn(Mono.just(mockResponse));
var webClient = WebClient.builder().exchangeFunction(exchangeFunction).build();
var adapter = WebClientAdapter.create(webClient);
var factory = HttpServiceProxyFactory.builderFor(adapter).build();
var client = factory.createClient(TestClient.class);
Assertions.assertDoesNotThrow(() -> client.get(Map.of("userId:eq", "test")));
}
}
When a URL variable name is transformed to "{userId:eq}" on line 499 of the file linked above, the UriComponentsBuilder interprets this as "variable userId with default value "eq""
This causes the following exception with Spring Framework 6.2.2
Caused by: java.lang.IllegalArgumentException: Map has no value for 'userId'
at org.springframework.web.util.UriComponents$MapTemplateVariables.getValue(UriComponents.java:348)
at org.springframework.web.util.HierarchicalUriComponents$QueryUriTemplateVariables.getValue(HierarchicalUriComponents.java:1099)
at org.springframework.web.util.UriComponents.expandUriComponent(UriComponents.java:263)
at org.springframework.web.util.HierarchicalUriComponents.lambda$expandQueryParams$5(HierarchicalUriComponents.java:453)
at org.springframework.util.UnmodifiableMultiValueMap.lambda$forEach$0(UnmodifiableMultiValueMap.java:115)
at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:986)
at org.springframework.util.MultiValueMapAdapter.forEach(MultiValueMapAdapter.java:179)
at org.springframework.util.UnmodifiableMultiValueMap.forEach(UnmodifiableMultiValueMap.java:115)
at org.springframework.web.util.HierarchicalUriComponents.expandQueryParams(HierarchicalUriComponents.java:452)
at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:441)
at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:53)
at org.springframework.web.util.UriComponents.expand(UriComponents.java:161)
at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.build(DefaultUriBuilderFactory.java:447)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.uri(DefaultWebClient.java:240)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.uri(DefaultWebClient.java:201)
at org.springframework.web.reactive.function.client.support.WebClientAdapter.newRequest(WebClientAdapter.java:121)
at org.springframework.web.reactive.function.client.support.WebClientAdapter.exchangeForBodyMono(WebClientAdapter.java:78)
at org.springframework.web.service.invoker.HttpServiceMethod$ReactorExchangeResponseFunction.lambda$initBodyFunction$5(HttpServiceMethod.java:554)
at org.springframework.web.service.invoker.HttpServiceMethod$ReactorExchangeResponseFunction.execute(HttpServiceMethod.java:449)
at org.springframework.web.service.invoker.HttpServiceMethod.invoke(HttpServiceMethod.java:133)
at org.springframework.web.service.invoker.HttpServiceProxyFactory$HttpServiceMethodInterceptor.invoke(HttpServiceProxyFactory.java:243)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)