OpenFeign / feign

Feign makes writing java http clients easier
Apache License 2.0
9.46k stars 1.92k forks source link

Unable to send empty JSON object with `@Body` #2041

Open otbutz opened 1 year ago

otbutz commented 1 year ago

Both approaches don't seem to work using feign 12.3 with okhttp and GSON encoder/decoder:

@Headers({"Accept: application/json", "Content-Type: application/json"})
public interface Api {

    @RequestLine("POST /empty")
    @Body("%7B%7D")
    void postEncoded();

    @RequestLine("POST /empty")
    @Body("{}")
    void postUnencoded();
}

The first sends it verbatim, which can't be parsed by the server, and the second triggers the expansion logic, creating an exception:

java.lang.IllegalArgumentException: an expression is required.
    at feign.template.Expressions.create(Expressions.java:68) ~[feign-core-12.3.jar:?]
    at feign.template.Template.parseFragment(Template.java:218) ~[feign-core-12.3.jar:?]
    at feign.template.Template.parseTemplate(Template.java:202) ~[feign-core-12.3.jar:?]
    at feign.template.Template.<init>(Template.java:61) ~[feign-core-12.3.jar:?]
    at feign.template.BodyTemplate.<init>(BodyTemplate.java:54) ~[feign-core-12.3.jar:?]
    at feign.template.BodyTemplate.create(BodyTemplate.java:50) ~[feign-core-12.3.jar:?]
    at feign.RequestTemplate.bodyTemplate(RequestTemplate.java:909) ~[feign-core-12.3.jar:?]
    at feign.Contract$Default.lambda$new$2(Contract.java:285) ~[feign-core-12.3.jar:?]
    at feign.DeclarativeContract$GuardedAnnotationProcessor.process(DeclarativeContract.java:253) ~[feign-core-12.3.jar:?]
    at feign.DeclarativeContract.lambda$processAnnotationOnMethod$7(DeclarativeContract.java:93) ~[feign-core-12.3.jar:?]
    at java.util.ArrayList.forEach(ArrayList.java:1511) ~[?:?]
    at feign.DeclarativeContract.processAnnotationOnMethod(DeclarativeContract.java:93) ~[feign-core-12.3.jar:?]
    at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:110) ~[feign-core-12.3.jar:?]
    at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:65) ~[feign-core-12.3.jar:?]
    at feign.DeclarativeContract.parseAndValidateMetadata(DeclarativeContract.java:38) ~[feign-core-12.3.jar:?]
    at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:134) ~[feign-core-12.3.jar:?]
    at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:56) ~[feign-core-12.3.jar:?]
    at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:48) ~[feign-core-12.3.jar:?]
    at feign.Feign$Builder.target(Feign.java:196) ~[feign-core-12.3.jar:?]
    at feign.Feign$Builder.target(Feign.java:192) ~[feign-core-12.3.jar:?]
otbutz commented 1 year ago

According to the javadoc, I would have expected the first variant to work?

https://github.com/OpenFeign/feign/blob/ae16dda47ee7fcb84d4d1cc843842fb7314598bd/core/src/main/java/feign/Body.java#L33

kdavisk6 commented 1 year ago

The first method will post the pct-encoded values verbatim, as you've discovered, because the we do not decode already encoded data. The second will not work as well, since the body will be parsed like an expression.

You may need to look into providing a custom Encoder instance that can generate the empty JSON document instead of using the @Body annotation. Review the documentation on the @Body annotation for more hints. You may need to explicitly set a Content-Type header.

The first sends it verbatim, which can't be parsed by the server

The request should be parsed by a server that accepts pct-encoding, which should be every HTTP compliant server out there, so this may be something to look into.

otbutz commented 1 year ago

The first method will post the pct-encoded values verbatim, as you've discovered, because the we do not decode already encoded data. The second will not work as well, since the body will be parsed like an expression.

You may need to look into providing a custom Encoder instance that can generate the empty JSON document instead of using the @Body annotation.

So there is no way to simply provide a non-expression based JSON document using the @Body annotation?

You may need to explicitly set a Content-Type header.

The @Headers annotation is present at the type level:

@Headers({"Accept: application/json", "Content-Type: application/json"})
public interface Api {

The request should be parsed by a server that accepts pct-encoding, which should be every HTTP compliant server out there, so this may be something to look into.

At least the API i'm calling doesn't support this and I can't change that. But isn't this limited to application/x-www-form-urlencoded ?

It would be nice to have an optional parameter for the @Body annotation to force expression parsing. e.g

@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void json(@Param("user_name") String user, @Param("password") String password);

@Body("%7B\"user_name\": \"denominator\", \"password\": \"secret\"%7D", forceExpression = true)
void json(@Param("user_name") String user, @Param("password") String password);