Search code examples
javaspring-boothttp-getfeignopenfeign

Spring Boot OpenFeign: java.lang.IllegalStateException: Method has too many Body parameters


The examples below demonstrate the issues experienced when using OpenFeign. The problem becomes evident when your response object has too many fields, which throws an error: Method has too many parameters. Example 1 works perfectly, but Example 2 fails.

Example 1 uses HTTP.POST with the same response object as used in Example 2, which uses HTTP.GET.

Why does OpenFeign limit the fields in an HTTP.GET method, and throw an exception? I can't use an HTTP.POST to get/fetch/read a resource. BAD REST API Design standards.

Using the same response object, for both HTTP.POST (which works), HTTP.GET fails

public interface ClientFeignV2 {

//Example 1 
@Headers("Content-Type: application/json") @RequestLine("POST api/v2/clients") ClientResponse findAllClientsByUid1(@RequestBody ClientRequest request);

//Example 2
@Headers("Content-Type: application/json")
@RequestLine("GET api/v2/clients/{uid}")
ClientResponse findAllClientsByUid(@PathVariable(value = "uid") String uid,
                                         @RequestParam(value = "limit", required = false) Integer limit,
                                         @RequestParam(value = "offset", required = false) Integer offset);
}

StackTrace:

Caused by: java.lang.IllegalStateException: Method has too many Body parameters: public abstract com.services.requestresponse.ClientResponse com.microservice.gateway.feign.v2.ClientFeignV2.findAllClientsByUid(java.lang.String,java.lang.Integer,java.lang.Integer)
at feign.Util.checkState(Util.java:128) ~[feign-core-9.4.0.jar:na]
at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:114) ~[feign-core-9.4.0.jar:na]
at feign.Contract$BaseContract.parseAndValidatateMetadata(Contract.java:64) ~[feign-core-9.4.0.jar:na]
at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:146) ~[feign-core-9.4.0.jar:na]
at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:53) ~[feign-core-9.4.0.jar:na]
at feign.Feign$Builder.target(Feign.java:209) ~[feign-core-9.4.0.jar:na]
at feign.Feign$Builder.target(Feign.java:205) ~[feign-core-9.4.0.jar:na]
at com.microservice.gateway.service.v2.impl.ClientServiceV2Impl.<init>(ClientServiceV2Impl.java:27) ~[classes/:na]
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_222]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_222]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_222]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_222]
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:203) ~[spring-beans-5.2.3.RELEASE.jar:5.2.3.RELEASE]
... 40 common frames omitted

I went through the OpenFeign documentation, and it supports the above implementation. If I can't find a solution to this issue, I would have to resort to a workaround and use HTTP.POST and @RequestBody which is not an ideal solution according to Rest-API design standards.


Solution

  • According to the tags, you're using Spring Boot, obviously with Spring Cloud OpenFeign. And the thing is that you're mixing two different Feign contracts.

    Annotations like @RequestLine and @Headers are from the core feign library. You can use it as a declarative HTTP client, and not only in Spring applications (it won't need to use Spring annotations in that case).

    So, the correct "example 2" with plain Feign could be like:

    @Headers("Content-Type: application/json")
    @RequestLine("GET api/v2/clients/{uid}?limit={limit}&offset={offset}")
    ClientResponse findAllClientsByUid(@Param("uid") String uid,
                                       @Param("limit") Integer limit,
                                       @Param("offset") Integer offset);
    

    On the other hand, things like @RequestParam and @PathVariable are from Spring Web. They can be used if you have Spring Cloud OpenFeign library (the core feign is one of its components btw). This library brings support for SpringMvcContract, which, in its case, allows you to use usual Spring Web annotations for defining request mapping instead of Feign-specific ones.

    In case of SpringMvcContract, "example 2" could be like:

    @GetMapping(value = "api/v2/clients/{uid}", consumes = MediaType.APPLICATION_JSON_VALUE)
    ClientResponse findAllClientsByUid(@PathVariable(value = "uid") String uid,
                    @RequestParam(value = "limit", required = false) Integer limit,
                    @RequestParam(value = "offset", required = false) Integer offset);
    

    It's worth to mention that in Spring Cloud OpenFeign the second approach is used by default. To change it back to plain Feign contract, define the custom Contract bean (source):

    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }