Spring-cloud-netflix: Support different custom configurations for multiple feign clients with the same name

Created on 25 Jul 2016  ·  37Comments  ·  Source: spring-cloud/spring-cloud-netflix

using @FeignClient in spring currently has one fatal issue, which is
demonstrated with this repository. If I got two feign clients, each having it's own configuration (excluded from component scan), and one of them has an RequestInterceptor, both (and all others, if present) get this interceptor also applied. The repository shows the following experiment to show this:

experiment

We assume some foreign service, serving "foo" entities. You have to be authenticated
to access this the foo resource and have a role "USER". If you have "ADMIN", you
will see different data, as when "USER".

This behavior is implemented using plain spring boot with spring security in
the "producer" service. Additionally it registers to an eureka instance, so some
"consumer" service is able to fetch this.

So the consumer service should fetch data from consumer, automatically passing
basic auth of "users he know". While this may not clearly makes sense for real world,
something very similar is happening consuming OAuth2 secured resource servers with
client credentials grant!
So to get the data correctly, we define 2 clients, with individually set up
basic auth credentials using feign client configuration.

Since the main application
does not use @SpringBootApplication, but @EnableAutoConfiguration,
and an exclude filter on @ComponentScan, the two different configuration
do not declare the internal beans.

So the expected result of these 2 clients is: the UserFooClient retrieving
at least a foo entity with value "user1", while the AdminFooClients result
should contain a "admin1" foo.

To verify, which result is intended, the same test is done using plain old
RestTemplate.

consequences

This issue leads towards we can't really use multiple feign clients in spring boot
using the @FeignClient annotation, because as soon one client is using
some authorization flow (basic, oauth2), this is applied to all other
clients.

documentation enhancement

Most helpful comment

@xetys Based on @spencergibb's suggestion you can do something like this to accomplish what you want

        @Autowired
        public FooController(
                ResponseEntityDecoder decoder, SpringEncoder encoder, EurekaClient discoveryClient) {
            InstanceInfo prodSvcInfo =  discoveryClient.getNextServerFromEureka("PROD-SVC", false);
            this.fooClient = Feign.builder()
                    .encoder(encoder)
                    .decoder(decoder)
                     .requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
                     .target(FooClient.class, prodSvcInfo.getHomePageUrl());
            this.adminClient = Feign.builder()
                    .encoder(encoder)
                    .decoder(decoder)
                     .requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
                     .target(FooClient.class, prodSvcInfo.getHomePageUrl());
        }

You could probably also integrate Ribbon into the situation as well.

All 37 comments

My first setup from yesterday was not the right approach. I have updated it now to a setup of 2 spring cloud microservices, do reconstruct the failure. The experiment changed a bit.

Just run the script provided in that repository, this should make things easy enough to test

@xetys I was looking at this last night, and I thought I had reproduced the problem with your original code. I ended up writing my own users service running on port 3000 and I saw auth headers being send regardless of which endpoint I used. So what about that sample scenario was wrong?

I failed to setup the excludeFilter properly, so the "excluded" configuration actually were included. Reparing this also made that example workinng.

it starts breaking when ribbon balancing is involved.

Ah ok so its only a problem with feign + ribbon, hence why you added Eureka into the mix. I will take a look again

sorry, if it was confusing. i first tried to use SpringApplicationBuilder to reproduce, but this would force me to spend more time on this I currently can do :/

You don't need all that infrastructure to reproduce this problem. All you need is 2 feign clients with the same name and different configurations (they can't actually have different configuration because the configuration is keyed on the name).

I think maybe to implement this change we could support an optional separation between the name of the feign client and the service id. Either by un-deprecating the serviceId attirbute, or by allowing user to specify a url with a serviceId instead of a host?

this is what confused me. with "name" i was thinking of the service id, but didn't used that because of decprecation

I have been thinking more about the correct solution to this problem....

To me it feels like there should not be a need to define multiple Feign Clients for a single service. It feels cleaner to have a single client for the service that is used throughout the application.

To fix the problem in your sample using a single Feign Client I attempted to define my own RequestInterceptor and put the logic about whether to send the admin creds vs the user creds in there. However by the time the apply method is called you dont have any knowledge of the Feign Client that the RequestInterceptor is being applied to and since the path and HTTP method are the same in both cases there is not enough information available to the RequestInterceptor to tell what credentials to use.

I think the cleanest solution to the problem would be able to specify a RequestInterceptor to be used at the FeignClient method level instead of just generally at the FeignClient level. For example, I was thinking something like this....

@FeignClient(name = "prod-svc")
public interface FooAdminClient extends FooClient {

   @RequestMapping("/foos", requestInterceptors={UserBasicAuthInterceptor.class, XyzRequestInterceptor.class})
    List<Foo> getUserFoos();

@RequestMapping("/foos", requestInterceptors={AdminBasicAuthInterceptor.class, AbcRequestInterceptor.class})
    List<Foo> getAdminFoos();
}

Of course you can still define RequestInterceptors at a Feign Client Config level as well which will be applied to add requests made from that Feign Client. You could also then potentially use this approach to override functionality of a general RequestInterceptor from a Feign Client Configuration with something that is more specific defined at a method level.

Thoughts?

Maybe @adriancole might have some thoughts as well?

I hate using class literals for dependencies, and there is no such attribute in @RequestMapping (and we can't add it), so I don't think that will fly. Apart from that I guess I follow the argument (the client is logically the same).

Argh, i overlooked the fact that RequestMapping comes from Spring, completely agree. I will try and think of something better.

Just a quick thought, what about adding an @RequestInterceptors annotation....

@FeignClient(name = "prod-svc")
public interface FooClient extends FooClient {

   @RequestMapping("/foos")
   @RequestInterceptors(requestInterceptors={UserBasicAuthInterceptor.class, XyzRequestInterceptor.class})
    List<Foo> getUserFoos();

@RequestMapping("/foos")
@RequestInterceptors(requestInterceptors={AdminBasicAuthInterceptor.class, AbcRequestInterceptor.class})
    List<Foo> getAdminFoos();
}

I guess that would work. I'd be more interested in something that didn't have class literals as constants, or where the class literals were Spring config, not interceptors. Anyway, the implementation of anything that has per-method custom configuration is probably going to be a bit messy, so I'm not super keen yet. The abstraction is not available in Feign anyway as far as I know.

When you say "class literals were Spring config" do you mean like having the option of having a config object at the method level in the Feign Client and then registering the RequestInterceptors in that config? I suppose that would give us a place for additional configuration if we ever need it in the future.

I realize none of this exists in Feign yet, so it is likely going into OpenFeign.

@dsyer ^^^

I don't really like the idea of a config file per method, if you push me on that. I think it would be better to just figure out a better way to index the configurations, so they are not always per name (e.g. per interface or something).

@dsyer so are you advocating taking the approach of having multiple Feign Clients per service? I really feel like a single Feign Client per service is the right approach and configuring the client to deal with any possible interactions of that service. The reason why you would need multiple Feign Clients today is because the configuration options aren't flexible enough (ie don't allow you to pass 2 different sets of credentials for the same API call)

There's already an open issue to customize the name of the bean, that would take care of this right?

@spencergibb I suppose it would, but does it make sense to be creating 2 Feign Clients for a single service?

I don't see why not really. There's nothing in feign itself that ties a single interface to each remote host.

Customizing the bean name isn't the point though, we have to customize the feign client name (key used to look up the configuration).

The issue I've run into, is that configuration keys use the context name, like ribbon (foo.ribbon.ConnectTimeout where foo is the name of the client and context). Teasing them apart isn't simple.

I'm wondering if at this point it's easier for a dev to use the builder implicitly rather than trying to use @FeignClient.

@spencergibb do u have an example of how one would do that?

https://github.com/OpenFeign/feign/#basics We could document how to do it and use the Spring extensions.

So this is all possible to do today right, they are just bypassing the SC stuff and using Feign directly right?

yes

@xetys Based on @spencergibb's suggestion you can do something like this to accomplish what you want

        @Autowired
        public FooController(
                ResponseEntityDecoder decoder, SpringEncoder encoder, EurekaClient discoveryClient) {
            InstanceInfo prodSvcInfo =  discoveryClient.getNextServerFromEureka("PROD-SVC", false);
            this.fooClient = Feign.builder()
                    .encoder(encoder)
                    .decoder(decoder)
                     .requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
                     .target(FooClient.class, prodSvcInfo.getHomePageUrl());
            this.adminClient = Feign.builder()
                    .encoder(encoder)
                    .decoder(decoder)
                     .requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
                     .target(FooClient.class, prodSvcInfo.getHomePageUrl());
        }

You could probably also integrate Ribbon into the situation as well.

I know this is an old issue and has been closed, but the issue I am encountering is very similar, though not exact.
I have a Parent Service which includes 5 (or more) small spring app dependencies. All of these dependencies are responsible to invoke 5 (or more) separate Services and consume the data they expose. They all use declarative FeignClient, Ribbon load balancing and Eureka.
They all also make use of request interceptors to decorate the request headers.

When I include all these dependencies (via gradle) in my parent service and start it, all the request Interceptors (across all 5 or more dependencies) are applied to all FeignClients. Although all the Feign clients target different services and all of them are coming from different jars.
Is this expected?

I also tried using the feign:
client:
config:
feignName:
requestInterceptors

but that didnt work as well. Is the only way to fix this issue is to change the declarative Feign Clients to a manual builder?

Thanks

I believe so

I've come across a similar issue just yesterday.
We have a service accepts multi-part upload requests for one method.
We had another method that was accepting a JSON object ( request body).

@FeignClient(name = "ab-document-store", fallback = DocumentStoreFallback.class, configuration = MultiPartSupportConfiguration.class)
public interface DocumentStoreClient {

    @Headers("Content-Type:multipart/form-data")
    @RequestMapping(path = "/document-store/v1/upload", method = RequestMethod.POST) // , consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.MULTIPART_FORM_DATA_VALUE)
    DocumentMetaData uploadDocument(@RequestPart("files") @Param("files") MultipartFile files);

//other methods here

    @RequestMapping(path = "/document-store/v1/save/as/zip")
    DocumentMetaData saveDocumentsZip(@RequestBody ZipFileModel zipFileModel);

For multi-part to work, we were using a configuration class and an interceptor:

public class MultiPartSupportConfiguration {

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    public Encoder feignFormEncoder() {
        return new MultipartFormEncoder(new SpringEncoder(messageConverters));
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.HEADERS;
    }
}

public class MultipartFormEncoder extends SpringFormEncoder {

    public MultipartFormEncoder(Encoder delegate) {
        super(delegate);
    }

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        if(bodyType.equals(MultipartFile.class)) {
            template.header("Content-Type", "multipart/form-data");
            super.encode(object, bodyType, template);
            return;
        }
        super.encode(object, bodyType, template);

    }
}

But, we we are seeing a problem. Spring ( or feign) is forcing requests to go through "ReflectiveFeign$BuildFormEncodedTemplateFromArgs".
see: https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/ReflectiveFeign.java#L352 ).

This is removing all variables except request params ( formVariables).

So I tried to split my problem into two Feign clients with two different configuration ( for the same micro-service). And obviously this did not work. Is there a solution using the spring-cloud feign solution?

Being able to do by-method configuration would work. But also being able to make two feign different feign clients for the same service. The main architect does not want us to use openfeign in our custom starter.

Our current work-around was to limit all our input into form compatible variables (puke) .

@FeignClient(name = "ab-document-store", fallback = DocumentStoreFallback.class, configuration = MultiPartSupportConfiguration.class)
public interface DocumentStoreClient {

    @RequestMapping(path = "/document-store/v1/upload", method = RequestMethod.POST) // , consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.MULTIPART_FORM_DATA_VALUE)
    DocumentMetaData uploadDocument(@RequestPart("files") @Param("files") MultipartFile files);

    @RequestMapping(path = "/document-store/v1/save/as/zip" , method = RequestMethod.POST)
    DocumentMetaData saveDocumentsZip(@RequestParam(value="filePaths[]") String[] filePaths,
                                      @RequestParam(value="fileIds[]") String[] fileIds,
                                      @RequestParam(value="resultFileName") String resultFileName);
}

Please open a separate issue in the correct project https://github.com/spring-cloud/spring-cloud-openfeign

Sorry for check this issue again with you @ryanjbaxter , I Just meet this issue but little different with this one, I have two feign client and two configutation, and I have two decoder in my configration like below

public class WeChatMiniAppUserServiceFeignClientConfig {
@Bean(name = "miniAppUserDecoder")
public Decoder miniAppUserDecoder() {
return new MiniAppUserDecoder();
}
}
and

public class WeChatPayServiceFeignClientConfig {
@Bean(name = "weChatPayDecoder")
public Decoder weChatPayDecoder() {
return new WeChatPayDecoder();
}
}
It works well when I compile a runable jar in windows, but when I compile it in linux, the HystrixRuntimeException will be happen, and the core message is:

feign.codec.DecodeException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.hehe.washTruck.wechat.miniapp.model.WxMiniAppJscode2SessionResult] and content type [text/plain]

we have the seem JDK and mvn like below:
JDK:
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

MAVEN:

Maven home: D:\michaeldev_app\mavenapache-maven-3.5.4\bin..
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: D:\michaeldev_app\java\jdk1.8.0_181\jre
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

the spring-boot version is 1.5.10.RELEASE
and the spring-cloud version is Brixton.SR5

Please open an issue in the correct project and include a sample that reproduces the issue.

Ok Thanks

| |
孙威
邮箱:[email protected]
|

签名由 网易邮箱大师 定制

On 10/15/2018 23:04, Ryan Baxter wrote:

Please open an issue in the correct project and include a sample that reproduces the issue.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or mute the thread.

I Have the same isuue,
@FeignClient(name = Consts.APP_NAME_BIM, fallback = FeignFileServiceFallback.class,
configuration = {FeignModelDbFileService.MultipartSupportConfig.class, HystrixConfiguration.class})
public interface FeignFileService {
@Component
class MultipartSupportConfig {
@Bean
@Primary
@Scope(SCOPE_PROTOTYPE)
public Encoder multipartFormEncoder() {
return new SpringFormEncoder();
}
}

@PostMapping(value = "/otherService/getFile", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity }

return exception like:
class java.util.ArrayList is not a type supported by this encoder

@menelaosbgr thanks, it is working

@FeignClient(name = Consts.APP_NAME_BIM, fallback = FeignFileServiceFallback.class,
configuration = {FeignModelDbFileService.MultipartSupportConfig.class, HystrixConfiguration.class})
public interface FeignModelDbFileService {
@Component
class MultipartSupportConfig {
@Autowired
private ObjectFactory messageConverters;

    @Bean
    @Primary
    @Scope(SCOPE_PROTOTYPE)
    public Encoder multipartFormEncoder() {
        return new MultipartFormEncoder(new SpringEncoder(messageConverters));
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.HEADERS;
    }
}

class MultipartFormEncoder extends SpringFormEncoder {

    public MultipartFormEncoder(Encoder delegate) {
        super(delegate);
    }

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        if (bodyType.equals(MultipartFile.class)) {
            template.header("Content-Type", "multipart/form-data");
            super.encode(object, bodyType, template);
            return;
        }
        super.encode(object, bodyType, template);

    }
}

@PostMapping(value = "/otherService/getFile", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity }

How does this problem exist? There are multiple approaches to a service.Are they all in one java class?

Was this page helpful?
0 / 5 - 0 ratings