From 458c30cb63cc7b3a236929ca382e9b4df65532cf Mon Sep 17 00:00:00 2001 From: Andrea Mauro Date: Sat, 9 Mar 2024 10:45:01 +0100 Subject: [PATCH] Resolve property-dependent parameter names for exception messages Prior to this commit when a required parameter defined as a property or expression placeholder was missing, the exception thrown would refer to the placeholder instead of the resolved name. This change covers messaging handlers and web controllers, both blocking and reactive. It also fixes the error message when handling null values for non-required parameters, as well as in cases that need conversion. See gh-32323 Closes gh-32462 --- ...tractNamedValueMethodArgumentResolver.java | 6 +- ...tractNamedValueMethodArgumentResolver.java | 6 +- .../HeaderMethodArgumentResolverTests.java | 35 ++++++++- .../HeaderMethodArgumentResolverTests.java | 34 ++++++++- ...tractNamedValueMethodArgumentResolver.java | 6 +- ...uestHeaderMethodArgumentResolverTests.java | 37 +++++++++- ...questParamMethodArgumentResolverTests.java | 74 ++++++++++++++++++- .../AbstractNamedValueArgumentResolver.java | 8 +- ...uestHeaderMethodArgumentResolverTests.java | 47 +++++++++++- 9 files changed, 234 insertions(+), 19 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java index 054a9762049..decc8743f8c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java @@ -97,9 +97,9 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements SyncHa arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } - arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); + arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); @@ -113,7 +113,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements SyncHa arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java index 8f2febc4faf..cf25ac019ff 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java @@ -105,9 +105,9 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } - arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); + arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); @@ -121,7 +121,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, message); + handleMissingValue(resolvedName.toString(), nestedParameter, message); } } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java index 940d9a97f65..bf6c8904f6d 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java @@ -36,6 +36,7 @@ import org.springframework.messaging.support.NativeMessageHeaderAccessor; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.messaging.handler.annotation.MessagingPredicates.header; import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain; @@ -151,6 +152,37 @@ class HeaderMethodArgumentResolverTests { assertThat(result).isEqualTo(Optional.empty()); } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); + + assertThatThrownBy(() -> + resolveArgument(param, message)) + .isInstanceOf(MessageHandlingException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); + + assertThatThrownBy(() -> + resolver.resolveArgument(param, message)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + + @SuppressWarnings({"unchecked", "ConstantConditions"}) private T resolveArgument(MethodParameter param, Message message) { return (T) this.resolver.resolveArgument(param, message).block(Duration.ofSeconds(5)); @@ -165,7 +197,8 @@ class HeaderMethodArgumentResolverTests { @Header(name = "#{systemProperties.systemProperty}") String param4, String param5, @Header("foo") Optional param6, - @Header("nativeHeaders.param1") String nativeHeaderParam1) { + @Header("nativeHeaders.param1") String nativeHeaderParam1, + @Header(name = "${systemProperty}", required = false) int primitivePlaceholderParam) { } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java index 713d9600fd8..ed4336b4c66 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java @@ -35,6 +35,7 @@ import org.springframework.messaging.support.NativeMessageHeaderAccessor; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.messaging.handler.annotation.MessagingPredicates.header; import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain; @@ -137,6 +138,36 @@ class HeaderMethodArgumentResolverTests { } } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); + + assertThatThrownBy(() -> + resolver.resolveArgument(param, message)) + .isInstanceOf(MessageHandlingException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg(); + + assertThatThrownBy(() -> + resolver.resolveArgument(param, message)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } + @Test void resolveOptionalHeaderWithValue() throws Exception { Message message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build(); @@ -162,7 +193,8 @@ class HeaderMethodArgumentResolverTests { @Header(name = "#{systemProperties.systemProperty}") String param4, String param5, @Header("foo") Optional param6, - @Header("nativeHeaders.param1") String nativeHeaderParam1) { + @Header("nativeHeaders.param1") String nativeHeaderParam1, + @Header(name = "${systemProperty}", required = false) int primitivePlaceholderParam) { } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java index c82ec2fe031..f8a08687bde 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -123,10 +123,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); + handleMissingValue(resolvedName.toString(), nestedParameter, webRequest); } if (!hasDefaultValue) { - arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); + arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType()); } } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { @@ -142,7 +142,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle arg = convertIfNecessary(parameter, webRequest, binderFactory, namedValueInfo, arg); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { - handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest); + handleMissingValueAfterConversion(resolvedName.toString(), nestedParameter, webRequest); } } } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java index e9bc98e3506..41f4e955160 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -69,6 +69,7 @@ class RequestHeaderMethodArgumentResolverTests { private MethodParameter paramInstant; private MethodParameter paramUuid; private MethodParameter paramUuidOptional; + private MethodParameter paramUuidPlaceholder; private MockHttpServletRequest servletRequest; @@ -93,6 +94,7 @@ class RequestHeaderMethodArgumentResolverTests { paramInstant = new SynthesizingMethodParameter(method, 8); paramUuid = new SynthesizingMethodParameter(method, 9); paramUuidOptional = new SynthesizingMethodParameter(method, 10); + paramUuidPlaceholder = new SynthesizingMethodParameter(method, 11); servletRequest = new MockHttpServletRequest(); webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); @@ -186,6 +188,20 @@ class RequestHeaderMethodArgumentResolverTests { } } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "bar"; + + System.setProperty("systemProperty", expected); + + assertThatThrownBy(() -> + resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null)) + .isInstanceOf(MissingRequestHeaderException.class) + .extracting("headerName").isEqualTo(expected); + + System.clearProperty("systemProperty"); + } + @Test void resolveDefaultValueFromRequest() throws Exception { servletRequest.setContextPath("/bar"); @@ -296,6 +312,24 @@ class RequestHeaderMethodArgumentResolverTests { assertThat(result).isNull(); } + @Test + public void uuidPlaceholderConversionWithEmptyValue() { + String expected = "name"; + servletRequest.addHeader(expected, ""); + + System.setProperty("systemProperty", expected); + + ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); + bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + + assertThatThrownBy(() -> + resolver.resolveArgument(paramUuidPlaceholder, null, webRequest, + new DefaultDataBinderFactory(bindingInitializer))) + .isInstanceOf(MissingRequestHeaderException.class) + .extracting("headerName").isEqualTo(expected); + + System.clearProperty("systemProperty"); + } void params( @RequestHeader(name = "name", defaultValue = "bar") String param1, @@ -308,7 +342,8 @@ class RequestHeaderMethodArgumentResolverTests { @RequestHeader("name") Date dateParam, @RequestHeader("name") Instant instantParam, @RequestHeader("name") UUID uuid, - @RequestHeader(name = "name", required = false) UUID uuidOptional) { + @RequestHeader(name = "name", required = false) UUID uuidOptional, + @RequestHeader(name = "${systemProperty}") UUID uuidPlaceholder) { } } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java index 82e75096525..c596777fd5a 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -23,6 +23,7 @@ import java.util.Optional; import jakarta.servlet.http.Part; import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.propertyeditors.StringTrimmerEditor; @@ -37,7 +38,9 @@ import org.springframework.web.bind.support.DefaultDataBinderFactory; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.bind.support.WebRequestDataBinder; import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.MissingServletRequestPartException; @@ -50,6 +53,7 @@ import org.springframework.web.testfixture.servlet.MockPart; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.web.testfixture.method.MvcAnnotationPredicates.requestParam; @@ -64,7 +68,7 @@ import static org.springframework.web.testfixture.method.MvcAnnotationPredicates */ class RequestParamMethodArgumentResolverTests { - private RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(null, true); + private RequestParamMethodArgumentResolver resolver; private MockHttpServletRequest request = new MockHttpServletRequest(); @@ -72,6 +76,15 @@ class RequestParamMethodArgumentResolverTests { private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + @BeforeEach + void setup() { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.refresh(); + resolver = new RequestParamMethodArgumentResolver(context.getBeanFactory(), true); + + // Expose request to the current thread (for SpEL expressions) + RequestContextHolder.setRequestAttributes(webRequest); + } @Test void supportsParameter() { @@ -141,6 +154,12 @@ class RequestParamMethodArgumentResolverTests { param = this.testMethod.annotPresent(RequestPart.class).arg(MultipartFile.class); assertThat(resolver.supportsParameter(param)).isFalse(); + + param = this.testMethod.annotPresent(RequestParam.class).arg(Integer.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(RequestParam.class).arg(int.class); + assertThat(resolver.supportsParameter(param)).isTrue(); } @Test @@ -678,6 +697,55 @@ class RequestParamMethodArgumentResolverTests { assertThat(actual).isEqualTo(Optional.empty()); } + @Test + void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); + + Integer expected = 100; + request.addParameter("name", expected.toString()); + + System.setProperty("systemProperty", "name"); + + try { + MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}")).arg(Integer.class); + Object result = resolver.resolveArgument(param, null, webRequest, binderFactory); + boolean condition = result instanceof Integer; + assertThat(condition).isTrue(); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "name"; + System.setProperty("systemProperty", expected); + + MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}")).arg(Integer.class); + assertThatThrownBy(() -> + resolver.resolveArgument(param, null, webRequest, null)) + .isInstanceOf(MissingServletRequestParameterException.class) + .extracting("parameterName").isEqualTo(expected); + + System.clearProperty("systemProperty"); + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + System.setProperty("systemProperty", expected); + + MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}").notRequired()).arg(int.class); + assertThatThrownBy(() -> + resolver.resolveArgument(param, null, webRequest, null)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + + System.clearProperty("systemProperty"); + } @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) public void handle( @@ -702,7 +770,9 @@ class RequestParamMethodArgumentResolverTests { @RequestParam("name") Optional paramOptionalArray, @RequestParam("name") Optional> paramOptionalList, @RequestParam("mfile") Optional multipartFileOptional, - @RequestParam(defaultValue = "false") Boolean booleanParam) { + @RequestParam(defaultValue = "false") Boolean booleanParam, + @RequestParam("${systemProperty}") Integer placeholderParam, + @RequestParam(name = "${systemProperty}", required = false) int primitivePlaceholderParam) { } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java index efa10c1405f..408ecee7aac 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java @@ -120,7 +120,7 @@ public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodAr return Mono.justOrEmpty(arg); }) .switchIfEmpty(getDefaultValue( - namedValueInfo, parameter, bindingContext, model, exchange)); + namedValueInfo, resolvedName.toString(), parameter, bindingContext, model, exchange)); } /** @@ -222,7 +222,7 @@ public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodAr /** * Resolve the default value, if any. */ - private Mono getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter, + private Mono getDefaultValue(NamedValueInfo namedValueInfo, String resolvedName, MethodParameter parameter, BindingContext bindingContext, Model model, ServerWebExchange exchange) { return Mono.fromSupplier(() -> { @@ -234,10 +234,10 @@ public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodAr value = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !parameter.isOptional()) { - handleMissingValue(namedValueInfo.name, parameter, exchange); + handleMissingValue(resolvedName, parameter, exchange); } if (!hasDefaultValue) { - value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType()); + value = handleNullValue(resolvedName, value, parameter.getNestedParameterType()); } if (value != null || !hasDefaultValue) { value = applyConversion(value, namedValueInfo, parameter, bindingContext, exchange); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index b1a956b6486..596dc36cd26 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -36,6 +36,7 @@ import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.reactive.BindingContext; +import org.springframework.web.server.MissingRequestValueException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; @@ -43,6 +44,7 @@ import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link RequestHeaderMethodArgumentResolver}. @@ -64,6 +66,7 @@ class RequestHeaderMethodArgumentResolverTests { private MethodParameter paramDate; private MethodParameter paramInstant; private MethodParameter paramMono; + private MethodParameter primitivePlaceholderParam; @BeforeEach @@ -87,6 +90,7 @@ class RequestHeaderMethodArgumentResolverTests { this.paramDate = new SynthesizingMethodParameter(method, 6); this.paramInstant = new SynthesizingMethodParameter(method, 7); this.paramMono = new SynthesizingMethodParameter(method, 8); + this.primitivePlaceholderParam = new SynthesizingMethodParameter(method, 9); } @@ -200,6 +204,46 @@ class RequestHeaderMethodArgumentResolverTests { } } + @Test + void missingParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + + System.setProperty("systemProperty", expected); + try { + Mono mono = this.resolver.resolveArgument( + this.paramResolvedNameWithExpression, this.bindingContext, exchange); + + assertThatThrownBy(() -> mono.block()) + .isInstanceOf(MissingRequestValueException.class) + .extracting("name").isEqualTo(expected); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() { + String expected = "sysbar"; + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + + System.setProperty("systemProperty", expected); + try { + Mono mono = this.resolver.resolveArgument( + this.primitivePlaceholderParam, this.bindingContext, exchange); + + assertThatThrownBy(() -> mono.block()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(expected); + } + finally { + System.clearProperty("systemProperty"); + } + } + @Test void notFound() { Mono mono = resolver.resolveArgument( @@ -252,7 +296,8 @@ class RequestHeaderMethodArgumentResolverTests { @RequestHeader("name") Map unsupported, @RequestHeader("name") Date dateParam, @RequestHeader("name") Instant instantParam, - @RequestHeader Mono alsoNotSupported) { + @RequestHeader Mono alsoNotSupported, + @RequestHeader(value = "${systemProperty}", required = false) int primitivePlaceholderParam) { } }