diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index bcf1dc165..4cf258c7c 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -44,6 +44,7 @@ public final class MethodMetadata implements Serializable { private transient Map indexToExpander; private BitSet parameterToIgnore = new BitSet(); private boolean ignored; + private boolean bodyRequired = true; private transient Class targetType; private transient Method method; private final transient List warnings = new ArrayList<>(); @@ -228,6 +229,14 @@ public boolean isIgnored() { return ignored; } + public boolean isBodyRequired() { + return bodyRequired; + } + + public void setBodyRequired(boolean bodyRequired) { + this.bodyRequired = bodyRequired; + } + @Experimental public MethodMetadata targetType(Class targetType) { this.targetType = targetType; diff --git a/core/src/main/java/feign/RequestTemplateFactoryResolver.java b/core/src/main/java/feign/RequestTemplateFactoryResolver.java index e39160b18..5b3f67bb9 100644 --- a/core/src/main/java/feign/RequestTemplateFactoryResolver.java +++ b/core/src/main/java/feign/RequestTemplateFactoryResolver.java @@ -261,7 +261,9 @@ protected RequestTemplate resolve( Object body = null; if (!alwaysEncodeBody) { body = argv[metadata.bodyIndex()]; - checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); + if (mutable.methodMetadata().isBodyRequired()) { + checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); + } } try { diff --git a/spring/src/main/java/feign/spring/SpringContract.java b/spring/src/main/java/feign/spring/SpringContract.java index 1f991e38d..4c44fbde9 100755 --- a/spring/src/main/java/feign/spring/SpringContract.java +++ b/spring/src/main/java/feign/spring/SpringContract.java @@ -116,6 +116,7 @@ public SpringContract() { registerParameterAnnotation( RequestBody.class, (body, data, paramIndex) -> { + data.setBodyRequired(body.required()); handleConsumesAnnotation(data, "application/json"); }); registerParameterAnnotation(RequestParam.class, requestParamParameterAnnotationProcessor()); diff --git a/spring/src/test/java/feign/spring/SpringContractTest.java b/spring/src/test/java/feign/spring/SpringContractTest.java index baa63a225..4f0277d70 100755 --- a/spring/src/test/java/feign/spring/SpringContractTest.java +++ b/spring/src/test/java/feign/spring/SpringContractTest.java @@ -76,6 +76,8 @@ void setup() throws IOException { .noContent(HttpMethod.GET, "/health/optional") .noContent(HttpMethod.GET, "/health/optional?param=value") .noContent(HttpMethod.GET, "/health/optional?param") + .noContent(HttpMethod.POST, "/health/withNonRequiredRequestBody") + .noContent(HttpMethod.POST, "/health/withRequiredRequestBody") .noContent(HttpMethod.GET, "/health/1?deep=true") .noContent(HttpMethod.GET, "/health/1?deep=true&dryRun=true") .noContent(HttpMethod.GET, "/health/name?deep=true&dryRun=true") @@ -166,6 +168,32 @@ void optionalNullable() { mockClient.verifyOne(HttpMethod.GET, "/health/optional"); } + @Test + void requiredRequestBodyIsNull() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, () -> resource.checkWithRequiredRequestBody(null)); + assertThat(exception.getMessage()).contains("Body parameter 0 was null"); + } + + @Test + void nonRequiredRequestBodyIsNull() { + resource.checkWithNonRequiredRequestBody(null); + + Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody"); + assertThat(request.requestTemplate().body()).asString().isEqualTo("null"); + } + + @Test + void nonRequiredRequestBodyIsObject() { + UserObject object = new UserObject(); + object.setName("hello"); + resource.checkWithNonRequiredRequestBody(object); + + Request request = mockClient.verifyOne(HttpMethod.POST, "/health/withNonRequiredRequestBody"); + assertThat(request.requestTemplate().body()).asString().contains("\"name\" : \"hello\""); + } + @Test void requestPart() { resource.checkRequestPart("1", "hello", "6"); @@ -302,6 +330,12 @@ void checkWithName( @RequestMapping(value = "/optional", method = RequestMethod.GET) void checkWithOptional(@RequestParam(name = "param") Optional param); + @RequestMapping(value = "/withNonRequiredRequestBody", method = RequestMethod.POST) + void checkWithNonRequiredRequestBody(@RequestBody(required = false) UserObject obj); + + @RequestMapping(value = "/withRequiredRequestBody", method = RequestMethod.POST) + void checkWithRequiredRequestBody(@RequestBody() UserObject obj); + @RequestMapping(value = "/part/{id}", method = RequestMethod.POST) void checkRequestPart( @PathVariable(name = "id") String campaignId, @@ -319,6 +353,19 @@ void checkRequestHeader( void checkRequestHeaderPojo(@RequestHeader HeaderMapUserObject object); } + class UserObject { + @Param("name1") + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + class HeaderMapUserObject { @Param("name1") private String name;