Skip to content

Commit

Permalink
Allow optional request body (#2616)
Browse files Browse the repository at this point in the history
* Allow optional request body

* Reformat code
  • Loading branch information
gromspys authored Nov 4, 2024
1 parent f76446d commit 20c77fc
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 1 deletion.
9 changes: 9 additions & 0 deletions core/src/main/java/feign/MethodMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public final class MethodMetadata implements Serializable {
private transient Map<Integer, Expander> 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<String> warnings = new ArrayList<>();
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/feign/RequestTemplateFactoryResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions spring/src/main/java/feign/spring/SpringContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public SpringContract() {
registerParameterAnnotation(
RequestBody.class,
(body, data, paramIndex) -> {
data.setBodyRequired(body.required());
handleConsumesAnnotation(data, "application/json");
});
registerParameterAnnotation(RequestParam.class, requestParamParameterAnnotationProcessor());
Expand Down
47 changes: 47 additions & 0 deletions spring/src/test/java/feign/spring/SpringContractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -302,6 +330,12 @@ void checkWithName(
@RequestMapping(value = "/optional", method = RequestMethod.GET)
void checkWithOptional(@RequestParam(name = "param") Optional<String> 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,
Expand All @@ -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;
Expand Down

0 comments on commit 20c77fc

Please sign in to comment.