Skip to content

Commit 51de84e

Browse files
OlgaMaciaszekrstoyanchev
authored andcommitted
Reject null for non-optional arguments
Closes gh-33339
1 parent 4ac4c1b commit 51de84e

File tree

14 files changed

+376
-79
lines changed

14 files changed

+376
-79
lines changed

framework-docs/modules/ROOT/pages/integration/rest-clients.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,9 @@ method parameters:
978978
`MultiValueMap<String, ?>` with multiple cookies, a `Collection<?>` of values, or an
979979
individual value. Type conversion is supported for non-String values.
980980

981+
The parameters can't be null unless they are set as not required through the annotation,
982+
annotated with `@Nullable` or they are `Optional`.
983+
981984
|===
982985

983986

framework-docs/modules/ROOT/pages/rsocket.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,8 @@ method parameters:
11151115
| `@Payload`
11161116
| Set the input payload(s) for the request. This can be a concrete value, or any producer
11171117
of values that can be adapted to a Reactive Streams `Publisher` via
1118-
`ReactiveAdapterRegistry`
1118+
`ReactiveAdapterRegistry`. The payload can't be null unless it's set as not required
1119+
through the annotation, annotated with `@Nullable` or it is `Optional`.
11191120

11201121
| `Object`, if followed by `MimeType`
11211122
| The value for a metadata entry in the input payload. This can be any `Object` as long

spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolver.java

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@
2929
* annotated arguments.
3030
*
3131
* @author Rossen Stoyanchev
32+
* @author Olga Maciaszek-Sharma
3233
* @since 6.0
3334
*/
3435
public class PayloadArgumentResolver implements RSocketServiceArgumentResolver {
@@ -54,25 +55,28 @@ public boolean resolve(
5455
return false;
5556
}
5657

57-
if (argument != null) {
58-
ReactiveAdapter reactiveAdapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
59-
if (reactiveAdapter == null) {
60-
requestValues.setPayloadValue(argument);
61-
}
62-
else {
63-
MethodParameter nestedParameter = parameter.nested();
64-
65-
String message = "Async type for @Payload should produce value(s)";
66-
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
67-
Assert.isTrue(!reactiveAdapter.isNoValue(), message);
58+
if (argument == null) {
59+
boolean required = (annot == null || annot.required()) && !parameter.isOptional();
60+
Assert.isTrue(!required, () -> "Missing payload");
61+
return true;
62+
}
6863

69-
requestValues.setPayload(
70-
reactiveAdapter.toPublisher(argument),
71-
ParameterizedTypeReference.forType(nestedParameter.getNestedGenericParameterType()));
72-
}
64+
ReactiveAdapter reactiveAdapter = this.reactiveAdapterRegistry
65+
.getAdapter(parameter.getParameterType());
66+
if (reactiveAdapter == null) {
67+
requestValues.setPayloadValue(argument);
7368
}
69+
else {
70+
MethodParameter nestedParameter = parameter.nested();
71+
72+
String message = "Async type for @Payload should produce value(s)";
73+
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
74+
Assert.isTrue(!reactiveAdapter.isNoValue(), message);
7475

76+
requestValues.setPayload(
77+
reactiveAdapter.toPublisher(argument),
78+
ParameterizedTypeReference.forType(nestedParameter.getNestedGenericParameterType()));
79+
}
7580
return true;
7681
}
77-
7882
}

spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolverTests.java

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.core.MethodParameter;
2626
import org.springframework.core.ParameterizedTypeReference;
2727
import org.springframework.core.ReactiveAdapterRegistry;
28+
import org.springframework.lang.Nullable;
2829
import org.springframework.messaging.handler.annotation.Payload;
2930

3031
import static org.assertj.core.api.Assertions.assertThat;
@@ -34,7 +35,9 @@
3435
* Tests for {@link PayloadArgumentResolver}.
3536
*
3637
* @author Rossen Stoyanchev
38+
* @author Olga Maciaszek-Sharma
3739
*/
40+
@SuppressWarnings("DataFlowIssue")
3841
class PayloadArgumentResolverTests extends RSocketServiceArgumentResolverTestSupport {
3942

4043
@Override
@@ -47,20 +50,15 @@ void stringPayload() {
4750
String payload = "payloadValue";
4851
boolean resolved = execute(payload, initMethodParameter(Service.class, "execute", 0));
4952

50-
assertThat(resolved).isTrue();
51-
assertThat(getRequestValues().getPayloadValue()).isEqualTo(payload);
52-
assertThat(getRequestValues().getPayload()).isNull();
53+
assertPayload(resolved, payload);
5354
}
5455

5556
@Test
5657
void monoPayload() {
5758
Mono<String> payloadMono = Mono.just("payloadValue");
5859
boolean resolved = execute(payloadMono, initMethodParameter(Service.class, "executeMono", 0));
5960

60-
assertThat(resolved).isTrue();
61-
assertThat(getRequestValues().getPayloadValue()).isNull();
62-
assertThat(getRequestValues().getPayload()).isSameAs(payloadMono);
63-
assertThat(getRequestValues().getPayloadElementType()).isEqualTo(new ParameterizedTypeReference<String>() {});
61+
assertPayloadMono(resolved, payloadMono);
6462
}
6563

6664
@Test
@@ -92,31 +90,77 @@ void completable() {
9290
}
9391

9492
@Test
95-
void notRequestBody() {
93+
void notPayload() {
9694
MethodParameter parameter = initMethodParameter(Service.class, "executeNotAnnotated", 0);
9795
boolean resolved = execute("value", parameter);
9896

9997
assertThat(resolved).isFalse();
10098
}
10199

102100
@Test
103-
void ignoreNull() {
104-
boolean resolved = execute(null, initMethodParameter(Service.class, "execute", 0));
101+
void nullPayload() {
102+
assertThatIllegalArgumentException()
103+
.isThrownBy(() -> execute(null, initMethodParameter(Service.class, "execute", 0)))
104+
.withMessage("Missing payload");
105+
106+
assertThatIllegalArgumentException()
107+
.isThrownBy(() -> execute(null, initMethodParameter(Service.class, "executeMono", 0)))
108+
.withMessage("Missing payload");
109+
}
110+
111+
@Test
112+
void nullPayloadWithNullable() {
113+
boolean resolved = execute(null, initMethodParameter(Service.class, "executeNullable", 0));
114+
assertNullValues(resolved);
115+
116+
boolean resolvedMono = execute(null, initMethodParameter(Service.class, "executeNullableMono", 0));
117+
assertNullValues(resolvedMono);
118+
}
119+
120+
@Test
121+
void nullPayloadWithNotRequired() {
122+
boolean resolved = execute(null, initMethodParameter(Service.class, "executeNotRequired", 0));
123+
assertNullValues(resolved);
105124

125+
boolean resolvedMono = execute(null, initMethodParameter(Service.class, "executeNotRequiredMono", 0));
126+
assertNullValues(resolvedMono);
127+
}
128+
129+
private void assertPayload(boolean resolved, String payload) {
130+
assertThat(resolved).isTrue();
131+
assertThat(getRequestValues().getPayloadValue()).isEqualTo(payload);
132+
assertThat(getRequestValues().getPayload()).isNull();
133+
}
134+
135+
private void assertPayloadMono(boolean resolved, Mono<String> payloadMono) {
136+
assertThat(resolved).isTrue();
137+
assertThat(getRequestValues().getPayloadValue()).isNull();
138+
assertThat(getRequestValues().getPayload()).isSameAs(payloadMono);
139+
assertThat(getRequestValues().getPayloadElementType()).isEqualTo(new ParameterizedTypeReference<String>() { });
140+
}
141+
142+
private void assertNullValues(boolean resolved) {
106143
assertThat(resolved).isTrue();
107144
assertThat(getRequestValues().getPayloadValue()).isNull();
108145
assertThat(getRequestValues().getPayload()).isNull();
109146
assertThat(getRequestValues().getPayloadElementType()).isNull();
110147
}
111148

112-
113-
@SuppressWarnings("unused")
149+
@SuppressWarnings({"unused"})
114150
private interface Service {
115151

116152
void execute(@Payload String body);
117153

154+
void executeNotRequired(@Payload(required = false) String body);
155+
156+
void executeNullable(@Nullable @Payload String body);
157+
118158
void executeMono(@Payload Mono<String> body);
119159

160+
void executeNullableMono(@Nullable @Payload Mono<String> body);
161+
162+
void executeNotRequiredMono(@Payload(required = false) Mono<String> body);
163+
120164
void executeSingle(@Payload Single<String> body);
121165

122166
void executeMonoVoid(@Payload Mono<Void> body);

spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* request header, path variable, cookie, and others.
4040
*
4141
* @author Rossen Stoyanchev
42+
* @author Olga Maciaszek-Sharma
4243
* @since 6.0
4344
*/
4445
public abstract class AbstractNamedValueArgumentResolver implements HttpServiceArgumentResolver {
@@ -145,7 +146,7 @@ private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValu
145146
.formatted(parameter.getNestedParameterType().getName()));
146147
}
147148
}
148-
boolean required = (info.required && !parameter.getParameterType().equals(Optional.class));
149+
boolean required = (info.required && !parameter.isOptional());
149150
String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
150151
return info.update(name, required, defaultValue);
151152
}

spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.web.service.invoker;
1818

19+
import java.util.Optional;
20+
1921
import org.apache.commons.logging.Log;
2022
import org.apache.commons.logging.LogFactory;
2123

@@ -41,17 +43,26 @@ public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver {
4143
public boolean resolve(
4244
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
4345

44-
if (!parameter.getParameterType().equals(HttpMethod.class)) {
46+
parameter = parameter.nestedIfOptional();
47+
48+
if (!parameter.getNestedParameterType().equals(HttpMethod.class)) {
4549
return false;
4650
}
4751

48-
Assert.notNull(argument, "HttpMethod is required");
52+
if (argument instanceof Optional<?> optionalValue) {
53+
argument = optionalValue.orElse(null);
54+
}
55+
56+
if (argument == null) {
57+
Assert.isTrue(parameter.isOptional(), "HttpMethod is required");
58+
return true;
59+
}
60+
4961
HttpMethod httpMethod = (HttpMethod) argument;
5062
requestValues.setHttpMethod(httpMethod);
5163
if (logger.isTraceEnabled()) {
5264
logger.trace("Resolved HTTP method to: " + httpMethod.name());
5365
}
54-
5566
return true;
5667
}
5768

spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.web.service.invoker;
1818

19+
import java.util.Optional;
20+
1921
import org.springframework.core.MethodParameter;
2022
import org.springframework.core.ParameterizedTypeReference;
2123
import org.springframework.core.ReactiveAdapter;
@@ -30,6 +32,7 @@
3032
* annotated arguments.
3133
*
3234
* @author Rossen Stoyanchev
35+
* @author Olga Maciaszek-Sharma
3336
* @since 6.0
3437
*/
3538
public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver {
@@ -68,33 +71,39 @@ public boolean resolve(
6871
return false;
6972
}
7073

71-
if (argument != null) {
72-
if (this.reactiveAdapterRegistry != null) {
73-
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
74-
if (adapter != null) {
75-
MethodParameter nestedParameter = parameter.nested();
76-
77-
String message = "Async type for @RequestBody should produce value(s)";
78-
Assert.isTrue(!adapter.isNoValue(), message);
79-
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
80-
81-
if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveRequestValues) {
82-
reactiveRequestValues.setBodyPublisher(
83-
adapter.toPublisher(argument), asParameterizedTypeRef(nestedParameter));
84-
}
85-
else {
86-
throw new IllegalStateException(
87-
"RequestBody with a reactive type is only supported with reactive client");
88-
}
89-
90-
return true;
91-
}
92-
}
74+
if (argument instanceof Optional<?> optionalValue) {
75+
argument = optionalValue.orElse(null);
76+
}
9377

94-
// Not a reactive type
95-
requestValues.setBodyValue(argument);
78+
if (argument == null) {
79+
Assert.isTrue(!annot.required() || parameter.isOptional(), "RequestBody is required");
80+
return true;
9681
}
9782

83+
if (this.reactiveAdapterRegistry != null) {
84+
ReactiveAdapter adapter = this.reactiveAdapterRegistry
85+
.getAdapter(parameter.getParameterType());
86+
if (adapter != null) {
87+
MethodParameter nestedParameter = parameter.nested();
88+
89+
String message = "Async type for @RequestBody should produce value(s)";
90+
Assert.isTrue(!adapter.isNoValue(), message);
91+
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
92+
93+
if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveRequestValues) {
94+
reactiveRequestValues.setBodyPublisher(
95+
adapter.toPublisher(argument), asParameterizedTypeRef(nestedParameter));
96+
}
97+
else {
98+
throw new IllegalStateException(
99+
"RequestBody with a reactive type is only supported with reactive client");
100+
}
101+
102+
return true;
103+
}
104+
}
105+
// Not a reactive type
106+
requestValues.setBodyValue(argument);
98107
return true;
99108
}
100109

spring-web/src/main/java/org/springframework/web/service/invoker/UriBuilderFactoryArgumentResolver.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,9 +17,11 @@
1717
package org.springframework.web.service.invoker;
1818

1919
import java.net.URL;
20+
import java.util.Optional;
2021

2122
import org.springframework.core.MethodParameter;
2223
import org.springframework.lang.Nullable;
24+
import org.springframework.util.Assert;
2325
import org.springframework.web.util.UriBuilderFactory;
2426
import org.springframework.web.util.UriTemplate;
2527

@@ -42,14 +44,22 @@ public class UriBuilderFactoryArgumentResolver implements HttpServiceArgumentRes
4244
public boolean resolve(
4345
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
4446

45-
if (!parameter.getParameterType().equals(UriBuilderFactory.class)) {
47+
parameter = parameter.nestedIfOptional();
48+
49+
if (!parameter.getNestedParameterType().equals(UriBuilderFactory.class)) {
4650
return false;
4751
}
4852

49-
if (argument != null) {
50-
requestValues.setUriBuilderFactory((UriBuilderFactory) argument);
53+
if (argument instanceof Optional<?> optionalValue) {
54+
argument = optionalValue.orElse(null);
55+
}
56+
57+
if (argument == null) {
58+
Assert.isTrue(parameter.isOptional(), "UriBuilderFactory is required");
59+
return true;
5160
}
5261

62+
requestValues.setUriBuilderFactory((UriBuilderFactory) argument);
5363
return true;
5464
}
5565
}

0 commit comments

Comments
 (0)