Feign Client is not so simple as we want – case study

The official documentation says „Feign is a declarative web service client. It makes writing web service clients easier”. I want to show you aspects on which you should focus when you want to use this tool to not make mistakes which I made and use it in a way that is easier, simpler and understandable.
It’s a case study, so grab your coffee and enjoy!
In my current project, we use Feign to communication with payments provider and our client, so it does not sound like a big deal… but we had some more or less complex problems when we had been configuring it.
It could be related to our team, which is not so much experienced in tools configuration, or with poor Feign documentation.
I tried to show you everything that I learned about this tool. Let’s go to the details!
I tried to show you everything that I learned about this tool. Let’s go to the details!
! Article based on spring-cloud-starter-openfeign in version 2.1.3 implemented in Spring Boot 2.1.8. !
Always use Feign.builder()
If you start to read about Feign clients you see there are two options to configure it – using @Bean annotation:
@Bean public Decoder feignDecoder() { return new ResponseEntityDecoder(new JacksonDecoder(getObjectMapper())); }
and using Feign.builder() as a Bean with all declarations:
return Feign.builder() .decoder(new ResponseEntityDecoder( new JacksonDecoder(customObjectMapper.customObjectMapper()))) .target(FeignRestClient.class, URL_ADDRESS);
Why I wrote about it?
As you remember in one application I wanted to use two Feign clients which call other API addresses. For one of them we had to add authorization, so we used request interceptor – method which attach customer token to all requests. We declared it in specific configuration to one of the clients using @Bean annotation, but second client also got this interceptor. It’s a known Feign problem and we found a jira task for that created in 2015…
So if you want to have less problems, use builder() – you will exactly know what is implemented – and for which client.
HTTP 302 redirect not so simple as you think
From our payment provider we receive request with http status 302 Found and then our problem starts, Feign returns an error:
Uncaught exception: feign.RetryableException: cannot retry due to redirection, in streaming mode executing POST
To solve this problem we need to tell Feign to not follow redirects using Request.Options:
int readTimeoutMillis = 10000; int connectTimeoutMillis = 5000; boolean followRedirects = false; return Feign.builder() .options(new Request.Options(connectTimeoutMillis, readTimeoutMillis, followRedirects)) .target(FeignRestClient.class, URL_ADDRESS);
And there we go! We got another error:
Uncaught exception: feign.FeignException: status 302 reading PayURestClient#createOrder(OrderRequest,String)
Yeah, set followRedirects to false does not solve our problems, cause Feign still treat this redirect as error so we need to create custom exception handler which catch this error for us.
public class FeignCustomErrorDecoder implements ErrorDecoder { private final ErrorDecoder errorDecoder = new Default(); @Override public Exception decode(String methodKey, Response response) { if (response.status() == 302) { throw new Response302Exception(response.reason(), response); } return errorDecoder.decode(methodKey, response); } }
Also we need to create Controller Advice and Service to get this
@RestControllerAdvice public class FeignExceptionHandler { @ExceptionHandler(value = Response302Exception.class) public ResponseEntity<?> handleResponseException(Response302Exception exception) { return ResponseEntity .status(exception.getResponse().status()) .body(exception.getResponse().body().toString()); } } Service: try { // this will always throw Exception, cause FEIGN have problem with 302 redirection FeignRestClient.createOrder(orderRequest, getBearerToken()); } catch (Response302Exception ex) { return orderResponse; }
And we need to extend our configuration with Feign error decoder:
return Feign.builder() .errorDecoder(new FeignCustomErrorDecoder()) .options(new Request.Options(connectTimeoutMillis, readTimeoutMillis, followRedirects)) .target(FeignRestClient.class, URL_ADDRESS);
Ok, now it works, we catch exception caused by 302 response using ErrorDecoder and we return correct response from it.
If we stand aside and look at it, it looks like some kind of hack…
Can we do something more with that to make it better?
I spent some time on talks and one of my colleagues after reviewing this article gave me advice to not use default Feign HTTP client and replace them by okHttpClient. So let’s check how it will work.
First lets build an interceptor for okHttpClient which will catch our response and change status for it to 200.
If we stand aside and look at it, it looks like some kind of hack…
Can we do something more with that to make it better?
I spent some time on talks and one of my colleagues after reviewing this article gave me advice to not use default Feign HTTP client and replace them by okHttpClient. So let’s check how it will work.
First lets build an interceptor for okHttpClient which will catch our response and change status for it to 200.
class RedirectInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); if (response.code() == 302) { return response.newBuilder().code(200).build(); } return response; } }
Then, let’s configure okHttpClient and add it to Feign configuration:
okhttp3.OkHttpClient okHttpClient = new okhttp3.OkHttpClient() .newBuilder() .addInterceptor(new RedirectInterceptor()) .build(); return Feign.builder() .client(new OkHttpClient(okHttpClient)) .target(FeignRestClient.class, URL_ADDRESS);
Service:
public OrderResponse createUOrder(OrderRequest orderRequest) { ResponseEntity<OrderResponse> orderResponse = FeignRestClient.createOrder(orderRequest, getBearerToken()); return orderResponse.getBody(); }
As you can see it looks better and cleaner, we don’t have any weird error encoders and our code is more readable now.
So if you suppose you will work with 302 redirects keep that in mind the Feign client doesn’t process it in simple way. In this examples I also show how to work with customized errors – this could be helpful in other cases.
If you want to know more about that, you can check here:
Where are my logs?
If you want to enable logging for Feign you need to attach logger, set log level in builder and also enable logging in application.properties:
builder: return Feign.builder() .logger(new Slf4jLogger(PayURestClient.class)) .logLevel(Logger.Level.FULL) .target(FeignRestClient.class, URL_ADDRESS); application.properties: logging.level.path.to.the.ClientClass=DEBUG
About log levels documentation says:
„The Logger.Level object that you may configure per client, tells Feign how much to log. Choices are:
NONE
, No logging (DEFAULT).BASIC
, Log only the request method and URL and the response status code and execution time.HEADERS
, Log the basic information along with request and response headers.FULL
, Log the headers, body, and metadata for both requests and responses.”How to work with JSON and Files?
To parse responses we need to use objects mapper. As you can suppose there is no possibility to get two mappers in one configuration, so you need to create two separate Feing clients, one for responses contains JSONs and second for media responses.
So it will look similar to this for JSON:
public ObjectMapper customObjectMapper(){ SimpleModule simpleModule = new SimpleModule(); ObjectMapper objectMapper = Jackson2ObjectMapperBuilder .json() .modules(new JavaTimeModule(), simpleModule) .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .build(); return objectMapper .setSerializationInclusion(JsonInclude.Include.NON_NULL); } return Feign.builder() .decoder(new ResponseEntityDecoder(new JacksonDecoder(customObjectMapper.customObjectMapper()))) .encoder(new JacksonEncoder(customObjectMapper.customObjectMapper())) .target(FeignRestClient.class, URL_ADDRESS);
And for file:
return Feign.builder() .decoder(new SpringDecoder(() -> new HttpMessageConverters(new ByteArrayHttpMessageConverter()))) .encoder(new JacksonEncoder(customObjectMapper.customObjectMapper())) .target(FeignRestClient.class, URL_ADDRESS);
Know your enemy
In conclusion, I don’t want to tell you „don’t use Feign”, but I hope these few points can be helpful before you start using this tool.
Maybe it will help you make a more informed decision about using this tool in your project, or maybe it will just make you aware of the pitfalls waiting for you, or maybe it will save you some time of the time I’ve devoted to it 🙂
Have you found any error or other solution for things described here, feel free to contact with me or leave a comment.
Najnowsze komentarze