Skip to content

Commit 458c30c

Browse files
andrea-maurosimonbasle
authored andcommitted
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
1 parent 5698191 commit 458c30c

File tree

9 files changed

+234
-19
lines changed

9 files changed

+234
-19
lines changed

Diff for: spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ public Object resolveArgumentValue(MethodParameter parameter, Message<?> message
9797
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
9898
}
9999
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
100-
handleMissingValue(namedValueInfo.name, nestedParameter, message);
100+
handleMissingValue(resolvedName.toString(), nestedParameter, message);
101101
}
102-
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
102+
arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType());
103103
}
104104
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
105105
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
@@ -113,7 +113,7 @@ else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
113113
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
114114
}
115115
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
116-
handleMissingValue(namedValueInfo.name, nestedParameter, message);
116+
handleMissingValue(resolvedName.toString(), nestedParameter, message);
117117
}
118118
}
119119
}

Diff for: spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ public Object resolveArgument(MethodParameter parameter, Message<?> message) thr
105105
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
106106
}
107107
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
108-
handleMissingValue(namedValueInfo.name, nestedParameter, message);
108+
handleMissingValue(resolvedName.toString(), nestedParameter, message);
109109
}
110-
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
110+
arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType());
111111
}
112112
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
113113
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
@@ -121,7 +121,7 @@ else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
121121
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
122122
}
123123
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
124-
handleMissingValue(namedValueInfo.name, nestedParameter, message);
124+
handleMissingValue(resolvedName.toString(), nestedParameter, message);
125125
}
126126
}
127127
}

Diff for: spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolverTests.java

+34-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import static org.assertj.core.api.Assertions.assertThat;
3838
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
39+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3940
import static org.springframework.messaging.handler.annotation.MessagingPredicates.header;
4041
import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain;
4142

@@ -151,6 +152,37 @@ void resolveOptionalHeaderAsEmpty() {
151152
assertThat(result).isEqualTo(Optional.empty());
152153
}
153154

155+
@Test
156+
void missingParameterFromSystemPropertyThroughPlaceholder() {
157+
String expected = "sysbar";
158+
System.setProperty("systemProperty", expected);
159+
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
160+
MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg();
161+
162+
assertThatThrownBy(() ->
163+
resolveArgument(param, message))
164+
.isInstanceOf(MessageHandlingException.class)
165+
.hasMessageContaining(expected);
166+
167+
System.clearProperty("systemProperty");
168+
}
169+
170+
@Test
171+
void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() {
172+
String expected = "sysbar";
173+
System.setProperty("systemProperty", expected);
174+
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
175+
MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg();
176+
177+
assertThatThrownBy(() ->
178+
resolver.resolveArgument(param, message))
179+
.isInstanceOf(IllegalStateException.class)
180+
.hasMessageContaining(expected);
181+
182+
System.clearProperty("systemProperty");
183+
}
184+
185+
154186
@SuppressWarnings({"unchecked", "ConstantConditions"})
155187
private <T> T resolveArgument(MethodParameter param, Message<?> message) {
156188
return (T) this.resolver.resolveArgument(param, message).block(Duration.ofSeconds(5));
@@ -165,7 +197,8 @@ public void handleMessage(
165197
@Header(name = "#{systemProperties.systemProperty}") String param4,
166198
String param5,
167199
@Header("foo") Optional<String> param6,
168-
@Header("nativeHeaders.param1") String nativeHeaderParam1) {
200+
@Header("nativeHeaders.param1") String nativeHeaderParam1,
201+
@Header(name = "${systemProperty}", required = false) int primitivePlaceholderParam) {
169202
}
170203

171204

Diff for: spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import static org.assertj.core.api.Assertions.assertThat;
3737
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
38+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3839
import static org.springframework.messaging.handler.annotation.MessagingPredicates.header;
3940
import static org.springframework.messaging.handler.annotation.MessagingPredicates.headerPlain;
4041

@@ -137,6 +138,36 @@ void resolveNameFromSystemProperty() throws Exception {
137138
}
138139
}
139140

141+
@Test
142+
void missingParameterFromSystemPropertyThroughPlaceholder() {
143+
String expected = "sysbar";
144+
System.setProperty("systemProperty", expected);
145+
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
146+
MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg();
147+
148+
assertThatThrownBy(() ->
149+
resolver.resolveArgument(param, message))
150+
.isInstanceOf(MessageHandlingException.class)
151+
.hasMessageContaining(expected);
152+
153+
System.clearProperty("systemProperty");
154+
}
155+
156+
@Test
157+
void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() {
158+
String expected = "sysbar";
159+
System.setProperty("systemProperty", expected);
160+
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
161+
MethodParameter param = this.resolvable.annot(header("${systemProperty}").required(false)).arg();
162+
163+
assertThatThrownBy(() ->
164+
resolver.resolveArgument(param, message))
165+
.isInstanceOf(IllegalStateException.class)
166+
.hasMessageContaining(expected);
167+
168+
System.clearProperty("systemProperty");
169+
}
170+
140171
@Test
141172
void resolveOptionalHeaderWithValue() throws Exception {
142173
Message<String> message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build();
@@ -162,7 +193,8 @@ public void handleMessage(
162193
@Header(name = "#{systemProperties.systemProperty}") String param4,
163194
String param5,
164195
@Header("foo") Optional<String> param6,
165-
@Header("nativeHeaders.param1") String nativeHeaderParam1) {
196+
@Header("nativeHeaders.param1") String nativeHeaderParam1,
197+
@Header(name = "${systemProperty}", required = false) int primitivePlaceholderParam) {
166198
}
167199

168200

Diff for: spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAn
123123
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
124124
}
125125
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
126-
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
126+
handleMissingValue(resolvedName.toString(), nestedParameter, webRequest);
127127
}
128128
if (!hasDefaultValue) {
129-
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
129+
arg = handleNullValue(resolvedName.toString(), arg, nestedParameter.getNestedParameterType());
130130
}
131131
}
132132
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
@@ -142,7 +142,7 @@ else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
142142
arg = convertIfNecessary(parameter, webRequest, binderFactory, namedValueInfo, arg);
143143
}
144144
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
145-
handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
145+
handleMissingValueAfterConversion(resolvedName.toString(), nestedParameter, webRequest);
146146
}
147147
}
148148
}

Diff for: spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java

+36-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class RequestHeaderMethodArgumentResolverTests {
6969
private MethodParameter paramInstant;
7070
private MethodParameter paramUuid;
7171
private MethodParameter paramUuidOptional;
72+
private MethodParameter paramUuidPlaceholder;
7273

7374
private MockHttpServletRequest servletRequest;
7475

@@ -93,6 +94,7 @@ void setup() throws Exception {
9394
paramInstant = new SynthesizingMethodParameter(method, 8);
9495
paramUuid = new SynthesizingMethodParameter(method, 9);
9596
paramUuidOptional = new SynthesizingMethodParameter(method, 10);
97+
paramUuidPlaceholder = new SynthesizingMethodParameter(method, 11);
9698

9799
servletRequest = new MockHttpServletRequest();
98100
webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());
@@ -186,6 +188,20 @@ void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception {
186188
}
187189
}
188190

191+
@Test
192+
void missingParameterFromSystemPropertyThroughPlaceholder() {
193+
String expected = "bar";
194+
195+
System.setProperty("systemProperty", expected);
196+
197+
assertThatThrownBy(() ->
198+
resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null))
199+
.isInstanceOf(MissingRequestHeaderException.class)
200+
.extracting("headerName").isEqualTo(expected);
201+
202+
System.clearProperty("systemProperty");
203+
}
204+
189205
@Test
190206
void resolveDefaultValueFromRequest() throws Exception {
191207
servletRequest.setContextPath("/bar");
@@ -296,6 +312,24 @@ private void uuidConversionWithEmptyOrBlankValueOptional(String uuid) throws Exc
296312
assertThat(result).isNull();
297313
}
298314

315+
@Test
316+
public void uuidPlaceholderConversionWithEmptyValue() {
317+
String expected = "name";
318+
servletRequest.addHeader(expected, "");
319+
320+
System.setProperty("systemProperty", expected);
321+
322+
ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
323+
bindingInitializer.setConversionService(new DefaultFormattingConversionService());
324+
325+
assertThatThrownBy(() ->
326+
resolver.resolveArgument(paramUuidPlaceholder, null, webRequest,
327+
new DefaultDataBinderFactory(bindingInitializer)))
328+
.isInstanceOf(MissingRequestHeaderException.class)
329+
.extracting("headerName").isEqualTo(expected);
330+
331+
System.clearProperty("systemProperty");
332+
}
299333

300334
void params(
301335
@RequestHeader(name = "name", defaultValue = "bar") String param1,
@@ -308,7 +342,8 @@ void params(
308342
@RequestHeader("name") Date dateParam,
309343
@RequestHeader("name") Instant instantParam,
310344
@RequestHeader("name") UUID uuid,
311-
@RequestHeader(name = "name", required = false) UUID uuidOptional) {
345+
@RequestHeader(name = "name", required = false) UUID uuidOptional,
346+
@RequestHeader(name = "${systemProperty}") UUID uuidPlaceholder) {
312347
}
313348

314349
}

Diff for: spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java

+72-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import jakarta.servlet.http.Part;
2525
import org.assertj.core.api.InstanceOfAssertFactories;
26+
import org.junit.jupiter.api.BeforeEach;
2627
import org.junit.jupiter.api.Test;
2728

2829
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
@@ -37,7 +38,9 @@
3738
import org.springframework.web.bind.support.WebDataBinderFactory;
3839
import org.springframework.web.bind.support.WebRequestDataBinder;
3940
import org.springframework.web.context.request.NativeWebRequest;
41+
import org.springframework.web.context.request.RequestContextHolder;
4042
import org.springframework.web.context.request.ServletWebRequest;
43+
import org.springframework.web.context.support.GenericWebApplicationContext;
4144
import org.springframework.web.multipart.MultipartException;
4245
import org.springframework.web.multipart.MultipartFile;
4346
import org.springframework.web.multipart.support.MissingServletRequestPartException;
@@ -50,6 +53,7 @@
5053

5154
import static org.assertj.core.api.Assertions.assertThat;
5255
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
56+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5357
import static org.mockito.BDDMockito.given;
5458
import static org.mockito.Mockito.mock;
5559
import static org.springframework.web.testfixture.method.MvcAnnotationPredicates.requestParam;
@@ -64,14 +68,23 @@
6468
*/
6569
class RequestParamMethodArgumentResolverTests {
6670

67-
private RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(null, true);
71+
private RequestParamMethodArgumentResolver resolver;
6872

6973
private MockHttpServletRequest request = new MockHttpServletRequest();
7074

7175
private NativeWebRequest webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
7276

7377
private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build();
7478

79+
@BeforeEach
80+
void setup() {
81+
GenericWebApplicationContext context = new GenericWebApplicationContext();
82+
context.refresh();
83+
resolver = new RequestParamMethodArgumentResolver(context.getBeanFactory(), true);
84+
85+
// Expose request to the current thread (for SpEL expressions)
86+
RequestContextHolder.setRequestAttributes(webRequest);
87+
}
7588

7689
@Test
7790
void supportsParameter() {
@@ -141,6 +154,12 @@ void supportsParameter() {
141154

142155
param = this.testMethod.annotPresent(RequestPart.class).arg(MultipartFile.class);
143156
assertThat(resolver.supportsParameter(param)).isFalse();
157+
158+
param = this.testMethod.annotPresent(RequestParam.class).arg(Integer.class);
159+
assertThat(resolver.supportsParameter(param)).isTrue();
160+
161+
param = this.testMethod.annotPresent(RequestParam.class).arg(int.class);
162+
assertThat(resolver.supportsParameter(param)).isTrue();
144163
}
145164

146165
@Test
@@ -678,6 +697,55 @@ void optionalMultipartFileWithoutMultipartRequest() throws Exception {
678697
assertThat(actual).isEqualTo(Optional.empty());
679698
}
680699

700+
@Test
701+
void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception {
702+
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
703+
initializer.setConversionService(new DefaultConversionService());
704+
WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer);
705+
706+
Integer expected = 100;
707+
request.addParameter("name", expected.toString());
708+
709+
System.setProperty("systemProperty", "name");
710+
711+
try {
712+
MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}")).arg(Integer.class);
713+
Object result = resolver.resolveArgument(param, null, webRequest, binderFactory);
714+
boolean condition = result instanceof Integer;
715+
assertThat(condition).isTrue();
716+
}
717+
finally {
718+
System.clearProperty("systemProperty");
719+
}
720+
}
721+
722+
@Test
723+
void missingParameterFromSystemPropertyThroughPlaceholder() {
724+
String expected = "name";
725+
System.setProperty("systemProperty", expected);
726+
727+
MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}")).arg(Integer.class);
728+
assertThatThrownBy(() ->
729+
resolver.resolveArgument(param, null, webRequest, null))
730+
.isInstanceOf(MissingServletRequestParameterException.class)
731+
.extracting("parameterName").isEqualTo(expected);
732+
733+
System.clearProperty("systemProperty");
734+
}
735+
736+
@Test
737+
void notNullablePrimitiveParameterFromSystemPropertyThroughPlaceholder() {
738+
String expected = "sysbar";
739+
System.setProperty("systemProperty", expected);
740+
741+
MethodParameter param = this.testMethod.annot(requestParam().name("${systemProperty}").notRequired()).arg(int.class);
742+
assertThatThrownBy(() ->
743+
resolver.resolveArgument(param, null, webRequest, null))
744+
.isInstanceOf(IllegalStateException.class)
745+
.hasMessageContaining(expected);
746+
747+
System.clearProperty("systemProperty");
748+
}
681749

682750
@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"})
683751
public void handle(
@@ -702,7 +770,9 @@ public void handle(
702770
@RequestParam("name") Optional<Integer[]> paramOptionalArray,
703771
@RequestParam("name") Optional<List<?>> paramOptionalList,
704772
@RequestParam("mfile") Optional<MultipartFile> multipartFileOptional,
705-
@RequestParam(defaultValue = "false") Boolean booleanParam) {
773+
@RequestParam(defaultValue = "false") Boolean booleanParam,
774+
@RequestParam("${systemProperty}") Integer placeholderParam,
775+
@RequestParam(name = "${systemProperty}", required = false) int primitivePlaceholderParam) {
706776
}
707777

708778
}

0 commit comments

Comments
 (0)