Skip to content

Commit 20c77fc

Browse files
authored
Allow optional request body (OpenFeign#2616)
* Allow optional request body * Reformat code
1 parent f76446d commit 20c77fc

File tree

4 files changed

+60
-1
lines changed

4 files changed

+60
-1
lines changed

core/src/main/java/feign/MethodMetadata.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public final class MethodMetadata implements Serializable {
4444
private transient Map<Integer, Expander> indexToExpander;
4545
private BitSet parameterToIgnore = new BitSet();
4646
private boolean ignored;
47+
private boolean bodyRequired = true;
4748
private transient Class<?> targetType;
4849
private transient Method method;
4950
private final transient List<String> warnings = new ArrayList<>();
@@ -228,6 +229,14 @@ public boolean isIgnored() {
228229
return ignored;
229230
}
230231

232+
public boolean isBodyRequired() {
233+
return bodyRequired;
234+
}
235+
236+
public void setBodyRequired(boolean bodyRequired) {
237+
this.bodyRequired = bodyRequired;
238+
}
239+
231240
@Experimental
232241
public MethodMetadata targetType(Class<?> targetType) {
233242
this.targetType = targetType;

core/src/main/java/feign/RequestTemplateFactoryResolver.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ protected RequestTemplate resolve(
261261
Object body = null;
262262
if (!alwaysEncodeBody) {
263263
body = argv[metadata.bodyIndex()];
264-
checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
264+
if (mutable.methodMetadata().isBodyRequired()) {
265+
checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
266+
}
265267
}
266268

267269
try {

spring/src/main/java/feign/spring/SpringContract.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public SpringContract() {
116116
registerParameterAnnotation(
117117
RequestBody.class,
118118
(body, data, paramIndex) -> {
119+
data.setBodyRequired(body.required());
119120
handleConsumesAnnotation(data, "application/json");
120121
});
121122
registerParameterAnnotation(RequestParam.class, requestParamParameterAnnotationProcessor());

spring/src/test/java/feign/spring/SpringContractTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ void setup() throws IOException {
7676
.noContent(HttpMethod.GET, "/health/optional")
7777
.noContent(HttpMethod.GET, "/health/optional?param=value")
7878
.noContent(HttpMethod.GET, "/health/optional?param")
79+
.noContent(HttpMethod.POST, "/health/withNonRequiredRequestBody")
80+
.noContent(HttpMethod.POST, "/health/withRequiredRequestBody")
7981
.noContent(HttpMethod.GET, "/health/1?deep=true")
8082
.noContent(HttpMethod.GET, "/health/1?deep=true&dryRun=true")
8183
.noContent(HttpMethod.GET, "/health/name?deep=true&dryRun=true")
@@ -166,6 +168,32 @@ void optionalNullable() {
166168
mockClient.verifyOne(HttpMethod.GET, "/health/optional");
167169
}
168170

171+
@Test
172+
void requiredRequestBodyIsNull() {
173+
Throwable exception =
174+
assertThrows(
175+
IllegalArgumentException.class, () -> resource.checkWithRequiredRequestBody(null));
176+
assertThat(exception.getMessage()).contains("Body parameter 0 was null");
177+
}
178+
179+
@Test
180+
void nonRequiredRequestBodyIsNull() {
181+
resource.checkWithNonRequiredRequestBody(null);
182+
183+
Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody");
184+
assertThat(request.requestTemplate().body()).asString().isEqualTo("null");
185+
}
186+
187+
@Test
188+
void nonRequiredRequestBodyIsObject() {
189+
UserObject object = new UserObject();
190+
object.setName("hello");
191+
resource.checkWithNonRequiredRequestBody(object);
192+
193+
Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody");
194+
assertThat(request.requestTemplate().body()).asString().contains("\"name\" : \"hello\"");
195+
}
196+
169197
@Test
170198
void requestPart() {
171199
resource.checkRequestPart("1", "hello", "6");
@@ -302,6 +330,12 @@ void checkWithName(
302330
@RequestMapping(value = "/optional", method = RequestMethod.GET)
303331
void checkWithOptional(@RequestParam(name = "param") Optional<String> param);
304332

333+
@RequestMapping(value = "/withNonRequiredRequestBody", method = RequestMethod.POST)
334+
void checkWithNonRequiredRequestBody(@RequestBody(required = false) UserObject obj);
335+
336+
@RequestMapping(value = "/withRequiredRequestBody", method = RequestMethod.POST)
337+
void checkWithRequiredRequestBody(@RequestBody() UserObject obj);
338+
305339
@RequestMapping(value = "/part/{id}", method = RequestMethod.POST)
306340
void checkRequestPart(
307341
@PathVariable(name = "id") String campaignId,
@@ -319,6 +353,19 @@ void checkRequestHeader(
319353
void checkRequestHeaderPojo(@RequestHeader HeaderMapUserObject object);
320354
}
321355

356+
class UserObject {
357+
@Param("name1")
358+
private String name;
359+
360+
public String getName() {
361+
return name;
362+
}
363+
364+
public void setName(String name) {
365+
this.name = name;
366+
}
367+
}
368+
322369
class HeaderMapUserObject {
323370
@Param("name1")
324371
private String name;

0 commit comments

Comments
 (0)