Search code examples
javaspring-bootfeign

Feign configuration not being used


I am using Feign in a Spring Boot API and I would like to configure it using the following:

import feign.RequestInterceptor;
import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.ErrorDecoder;
import feign.okhttp.OkHttpClient;
import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignClientConfiguration {

    @Bean
    public ErrorDecoder errorDecoder() {
        return new ErrorDecoder.Default();
    }

    @Bean
    public OkHttpClient client() {
        return new OkHttpClient();
    }

    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            requestTemplate.header("user", "some user");
            requestTemplate.header("password", "some password");
            requestTemplate.header("Accept", ContentType.APPLICATION_JSON.getMimeType());
        };
    }


}

Problem is that if I use the following code in a service I get a 401, seems like no authentication data was provided:

        Feign.builder()
        .client(new OkHttpClient())
        .encoder(new GsonEncoder())
        .decoder(new GsonDecoder())
        .logger(new Slf4jLogger(Npa.class))
        .target(AClass.class, "https://some.website");

If I just add one line specifying user and password it works:

    Feign.builder()
    .client(new OkHttpClient())
    .encoder(new GsonEncoder())
    .decoder(new GsonDecoder())
    .logger(new Slf4jLogger(Npa.class))
    .requestInterceptor(new BasicAuthRequestInterceptor("some user", "some password"))
    .target(AClass.class, "https://some.website");

I would prefer to use the configuration but I cannot understand what is wrong in it.


Solution

  • Since you've created some beans then you don't need to instantiate them again manually in builder steps.

    The pseudo-class where you can create your feign client could be like this:

    @Component
    public class FeignClientFactory {
    
        @Autowired
        private ErrorDecoder errorDecoder;
    
        @Autowired
        private OkHttpClient client;
    
        @Autowired
        private RequestInterceptor requestInterceptor;
    
        public AClass getFeignClient() {
            return Feign.builder()
                    .client(client)
                    .encoder(new GsonEncoder())
                    .decoder(new GsonDecoder())
                    .logger(new Slf4jLogger(Npa.class))
                    .requestInterceptor(requestInterceptor)
                    .target(AClass.class, "https://some.website");
        }
    }
    

    Of course, there is a lot of options to improve it. F.e., you can create other beans (for encoders, loggers and so on) or use the default (autoconfigured) ones.

    Or you can do it in @Configuration class if you need only one feign client bean.

    In case of request interceptors - there is another builder step .requestInterceptors, where you can pass a collection of them.

    EDIT: For basic authentication you don't need to have separate username and password headers but one header "Authorization" instead. Its value should look like "Basic username:password" where "username:password" part should be encoded with Base64 (link).

    So, in your @Configuration class you can write smth like this:

    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            String encodedCredentials = Base64.getEncoder()
                .encodeToString("some user:some password".getBytes());
            requestTemplate.header("Authorization", "Basic " + encodedCredentials);
            requestTemplate.header("Accept", ContentType.APPLICATION_JSON.getMimeType());
        };
    }
    

    Then you can use this bean for building Feign client as I described above.

    Another option: Because the creation of authorization header is a pretty common task, the BasicAuthRequestInterceptor was introduced by Feign which does the same. You can use it instead. So now, in @Configuration class you can create two beans:

        @Bean
        @Qualifier("basicAuthRequestInterceptor")
        public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
            return new BasicAuthRequestInterceptor("some user","some password");
        }
    
        @Bean
        @Qualifier("customRequestInterceptor")
        public RequestInterceptor requestInterceptor() {
            return requestTemplate -> {
                requestTemplate.header("Accept", ContentType.APPLICATION_JSON.getMimeType());
            };
        }
    

    In this case, you need to modify the feign client building method in order to apply both these interceptors:

    @Component
    public class FeignClientFactory {
    
        @Autowired
        private ErrorDecoder errorDecoder;
    
        @Autowired
        private OkHttpClient client;
    
        @Autowired
        @Qualifier("basicAuthRequestInterceptor")
        private RequestInterceptor basicAuthRequestInterceptor;
    
        @Autowired
        @Qualifier("customRequestInterceptor")
        private RequestInterceptor requestInterceptor;
    
        public AClass getFeignClient() {
            List<RequestInterceptor> requestInterceptors = new ArrayList<>();
            requestInterceptors.add(basicAuthRequestInterceptor);
            requestInterceptors.add(requestInterceptor);
    
            return Feign.builder()
                    .client(client)
                    .encoder(new GsonEncoder())
                    .decoder(new GsonDecoder())
                    .logger(new Slf4jLogger(Npa.class))
                    .requestInterceptors(requestInterceptors)
                    .target(AClass.class, "https://some.website");
        }
    }