Introduction
Feign is a declarative web service client. It makes the client implementation process fast. You can simply define a Java interface with a readable method names and annotations, and make it a functioning web client. You can refer to the readme[1] to have the basic knowledge on Feign. Also there are ample of blogs that you can refer. Through this post, I am going to explain on how you can achieve Error Decoding, and Retrying functionality in a Feign client.
Setting up dependencies
This post uses spring-cloud-starter Hoxton.RELEASE version of spring cloud. In the pom file, you need to add the spring-cloud-starter-parent as the parent-pom file and spring-cloud-dependencies as the dependency management. Spring-cloud-dependencies provide the spring-cloud dependency versions according to the parent pom version. Thereafter you need to add the following dependencies for the rest of implementation:
- spring-boot-starter
- spring-boot-starter-web
- spring-cloud-starter-openfeign
It’s worth mentioning that feign was created and maintained by Netflix OSS and currently maintained separately from Nextflix. Once you wire-up all dependencies, the final pom file would look like below:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-parent</artifactId> <version>Hoxton.RELEASE</version> </parent> <groupId>com.buddhima.testing</groupId> <artifactId>acn-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>acn-demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
Enable feign clients and define a feign client
To enable feign clients, you need to use @EnableFeignClients annotation in the main class definition. Then you can simply create an interface to the external web-services. In this post I’m not going to talk about the annotations because you can find a good documentation here [1][2]. Following is a sample interface I created for this post:
DrmServiceClient.java
package com.buddhima.testing.demo.client; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.List; @FeignClient(value="drmClient", url="http://www.mocky.io/v2/5e2d966a3000006200e77d2d") // to produce 400 HTTP status public interface DrmServiceClient { @RequestMapping(method = RequestMethod.GET, value = "/posts") List<Object> getObjects(); }
Enabling logging for feign client
First I decided to talk about logging as this helps to demonstrate the behaviors in next steps.
To enable extended logging for feign clients, you need to follow two steps.
- Enabling DEBUG log-level for feign client
- Change feign client log-level (valid values are NONE, BASIC, HEADERS, FULL)
application.yml
logging: level: com.buddhima.testing.demo.client: DEBUG feign: client: config: default: loggerLevel: BASIC
After this configuration, you can view Request-Response logs in the microservice log. You can further increase logging by changing the logger-level to HEADER or FULL.
Throughout this post, I am discussing about configuring the feign client through application.yml file. But there are multiple ways of doing the same.
Error Decoder for Feign client
You can use error-decoder to act based on the erroneous HTTP responses. I have observed that error-decoder does not get triggered on success scenarios. To implement an error-decoder, you need to implement a class using ErrorDecoder interface and add that in the configuration.
DrmClientErrorDecoder.java
package com.buddhima.testing.demo.client; import feign.Response; import feign.RetryableException; import feign.codec.ErrorDecoder; public class DrmClientErrorDecoder implements ErrorDecoder { private final ErrorDecoder defaultErrorDecoder = new Default(); @Override public Exception decode(String s, Response response) { System.out.println("Error Response!!!"); if (400 == response.status()) { System.out.println("It's a 400 Error!!!"); } return defaultErrorDecoder.decode(s, response); } }
application.yml
logging: level: com.buddhima.testing.demo.client: DEBUG feign: client: config: default: errorDecoder: com.buddhima.testing.demo.client.DrmClientErrorDecoder loggerLevel: BASIC
Retryer for Feign client
Retryer could be a useful entity to retry your request in case of failure (network failures by default). You can configure default retryer with parameters by extending Retryer.Default class.
DrmClientRetryer.java
package com.buddhima.testing.demo.client; import feign.Retryer; public class DrmClientRetryer extends Retryer.Default { public DrmClientRetryer() { super(); } }
application.yml
feign: client: config: default: errorDecoder: com.buddhima.testing.demo.client.DrmClientErrorDecoder loggerLevel: BASIC retryer: com.buddhima.testing.demo.client.DrmClientRetryer
Furthermore, if you need to retry based on HTTP status, you can throw RetryableException at error-decoder. Same goes, if you want to retry based on HTTP-headers.
DrmClientErrorDecoder.java
package com.buddhima.testing.demo.client; import feign.Response; import feign.RetryableException; import feign.codec.ErrorDecoder; public class DrmClientErrorDecoder implements ErrorDecoder { private final ErrorDecoder defaultErrorDecoder = new Default(); @Override public Exception decode(String s, Response response) { System.out.println("Error Response!!!"); if (400 == response.status()) { return new RetryableException(400, response.reason(), response.request().httpMethod(), null, response.request()); } return defaultErrorDecoder.decode(s, response); } }
Following is a sample log, which demonstrate re-trying 5 times before giving-up.
2020-01-27 23:20:41.317 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:42.153 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (836ms) Error Response!!! 2020-01-27 23:20:42.317 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:42.318 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:42.551 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (231ms) Error Response!!! 2020-01-27 23:20:42.776 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:42.777 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:43.062 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (285ms) Error Response!!! 2020-01-27 23:20:43.400 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:43.400 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:43.678 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (276ms) Error Response!!! 2020-01-27 23:20:44.185 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> RETRYING 2020-01-27 23:20:44.186 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] ---> GET http://www.mocky.io/v2/5e2d966a3000006200e77d2d/posts HTTP/1.1 2020-01-27 23:20:44.395 DEBUG 7429 --- [nio-8080-exec-1] c.b.t.demo.client.DrmServiceClient : [DrmServiceClient#getObjects] <--- HTTP/1.1 400 Bad Request (208ms)
In the above log, you can see that same request attempted 5 times back-to-back before it was declared as a failure. You can change the default values through the Retryer implementation to desired. Furthermore, you can alter the error-decoder to refer specific HTTP headers (eg: Retry-After header).
Conclusion
Through this post, I discussed on Feign clients with two different use-cases. First you have seen, how to react based on HTTP error statuses and later about retrying requests. I hope this will be beneficial for your spring boot application development.
I have uploaded the source-code used in this post at here: https://github.com/Buddhima/feign-demo
References
[1] https://github.com/OpenFeign/feign/blob/master/README.md
[2] https://cloud.spring.io/spring-cloud-openfeign/reference/html/
[3] http://www.matez.de/index.php/2017/04/12/exploring-feign-retrying/
In this tutorial, I will share with you how you can use Feign ErrorDecoder to handle errors that occur when using Feign client in Microservices communication.
For step-by-step video beginner lessons demonstrating how to do Feign error handling and how to build Microservices with Spring Boot and Spring Cloud, have a look at this page: Spring Boot Microservices and Spring Cloud.
Setup Feign
To make sure your Feign client works well and the errors you are getting are not caused by an incorrect setup of your Feign client, please have a look at the following tutorial to learn how to add Feign to your Spring Boot project and make it work: Feign Client to Call Another Microservice.
Create ErrorDecoder
To be able to use ErrorDecoder, you will need to create a new Java class and make it implement ErrorDecoder interface. Implementing ErrorDecoder interface gives you access to Method Key and Response objects.
- methodKey – will contain a Feign client class name and a method name,
- Response – will allow you to access the HTTP status code, the Body of HTTP Response and the Request object. You can use these details when handling an error message and preparing a response.
Below is an example of ErrorDecoder interface being implemented.
@Component public class FeignErrorDecoder implements ErrorDecoder { Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public Exception decode(String methodKey, Response response) { switch (response.status()){ case 400: logger.error("Status code " + response.status() + ", methodKey = " + methodKey); case 404: { logger.error("Error took place when using Feign client to send HTTP Request. Status code " + response.status() + ", methodKey = " + methodKey); return new ResponseStatusException(HttpStatus.valueOf(response.status()), "<You can add error message description here>"); } default: return new Exception(response.reason()); } } }
Please note the use of @Component annotation. If not using @Component annotation, you can also create Feign ErrorDecoder as a @Bean the following way:
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class PhotoAppApiApplication { public static void main(String[] args) { SpringApplication.run(PhotoAppApiApplication.class, args); } @Bean public FeignErrorDecoder errorDecoder() { return new FeignErrorDecoder(); } }
Now that your Feign ErrorDecoder interface is implemented, you can try using Feign client to send HTTP Request to a Web Service endpoint that does not exist and see if the 404 HTTP Status code is handled. You get a correct switch case executed, and the error message is logged.
Feign Client HTTP Requests Logging
When working with Feign clients, it is also very helpful to enable HTTP Requests logging. Read the following tutorial to learn how to enable Feign logging in your Spring Boot application:
- Feign HTTP Requests Logging
If you need to see how it is all done in step-by-step video lessons, checkout out at this page: Spring Boot Microservices and Spring Cloud.
I hope this tutorial was helpful to you.
There are many very good online video courses that teach how to build Spring Boot Microservices with Spring Cloud. Have a look at the list below and see if you like any of them.
Over the last couple of years, I’ve been using Feign to invoke HTTP APIs, let it be external or internal. If you are not familiar with Feign, here’s a very brief intro. Feign is a declarative HTTP client. You define an interface, take some magical annotations and you have yourself a fully functioning client that you can use to communicate via HTTP.
Feign is a standalone library, anybody can use it on a project. It comes with its own annotations, types and configuration. Here’s an example client from the docs:
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("POST /repos/{owner}/{repo}/issues") void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo); }
Having a tool to define APIs like this is a great way to reduce application complexity. Think about writing the same thing with Apache HttpComponents. Let me give you an idea:
CloseableHttpClient httpClient = HttpClients.createDefault(); try { String owner = ... String repo = ... HttpGet request = new HttpGet("https://api.github.com/repos/" + owner + "/" + repo +"/contributors"); CloseableHttpResponse response = httpClient.execute(request); try { HttpEntity entity = response.getEntity(); if (entity != null) { String result = EntityUtils.toString(entity); Gson gson = new Gson(); List<Contributor> contributors = gson.fromJson(result, new TypeToken<ArrayList<Contributor>>(){}.getType()); ... } } finally { response.close(); } } finally { httpClient.close(); }
I hope the difference is obvious. There’s tons of boilerplate code in the latter example.
Since Spring is one of the most used base frameworks in any Java project, there’s another level of abstraction that the Spring ecosystem provides. What if you don’t need to rely on the custom Feign annotations but you use the same Spring annotations just like when a controller is defined, e.g. @RequestMapping
, @PathVariable
and so on.
Well, Spring Cloud adds this capability to your application. The former example with Spring annotations looks the following:
@FeignClient("github") interface GitHub { @RequestMapping(value = "/repos/{owner}/{repo}/contributors", method = GET) List<Contributor> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo); @RequestMapping(value = "/repos/{owner}/{repo}/issues", method = POST) void createIssue(Issue issue, @PathVariable("owner") String owner, @PathVariable("repo") String repo); }
Quite neat compared to the previous examples.
Although, there’s one downside to any abstraction. Or maybe downside is not even the best word to describe it, its rather a trade-off that we – engineers – often forget.
One of the points of an abstraction is to hide details from its users to ease development, which is absolutely spot on in case of Feign. However, the trade-off you are going to make is less control over that particular piece of code. For normal use-cases it’s often perfectly fine but as soon as you hit a wall and you need some specific behavior, you have to start digging. Where? I guess most of us just Google for some time, hoping that somebody has asked a similar question on Stackoverflow. Sometimes it’s just not the case, and you have to jump right into the code to figure out how to work-around the framework.
Error handling in Feign
Fortunately the framework creators have thought about having some way of reacting to errors during an API call. The ErrorDecoder interface is used for that purpose.
public interface ErrorDecoder { public Exception decode(String methodKey, Response response); }
Such an interface implementation can be tied to creating a particular Feign client. In the standard Feign world, you can specify it during the Builder
calls, like:
Feign.builder() .decoder(new CustomDecoder())
So you can customize the Decoder you’d like to use on a per client basis, but not on a method basis. That’s just a limitation of the capabilities the library is providing.
How does this look in the Spring world? If you’d like to use an ErrorDecoder
, you can just simply register it as a bean and the framework will automatically pick it up and assign it to every Feign client.
@Configuration public class CustomFeignConfiguration { @Bean public CustomDecoder customDecoder() { return new CustomDecoder(); } }
Very neat, but since within an application there could be several Feign clients used, does it make sense to use a single decoder for all clients? Probably not. But even if you go one level deeper, you might need to have different error handling logic for different API calls within the same client.
The junior implementation
So if you were carefully reading, you might have noticed a parameter in the signature of the ErrorDecoder
, methodKey
. The methodKey
is automatically generated by the Feign library whenever an error response is received from the downstream API. An example methodKey
looks the following:
UserServiceClient#findById(UUID)
It starts with the Feign client class name, then a hash symbol and then the name of the method, followed by its parameter types within parentheses. The key generation can be found here: Feign#configKey
The first implementation one might think would be some kind of string magic on the methodKey:
@Override public Exception decode(String methodKey, Response response) { if (methodKey.startsWith("UserServiceClient")) { // dosomething specific to UserServiceClient } else if (...) { ... } }
Obviously this is going to work, but it won’t scale and as soon as you rename a class/method, you’ll end up in some functional problems in your application since the String renaming might be easily messed up (even though IDEs are very smart these days).
Testing is going to be hell with an implementation like this, cyclomatic complexity is going to increase with the number of clients and API methods. There must be a solution out there and somebody must have figured it out already.
Well, I thought the same, but so far I haven’t found anything on this.
The proper solution
First of all, this solution is aiming to address Spring only, when using the bare Feign client library, there are some minor tweaks required.
For a Spring Cloud based Feign client, you need to use the @FeignClient annotation on the interface like this:
@FeignClient(name = "user-service", url = "http://localhost:9002") public interface UserServiceClient { @GetMapping("/users/{id}") UserResponse findById(@PathVariable("id") UUID id) }
Pretty easy I’d say. So what happens when there’s an error, for example 404 returned by the API?
2020-09-29 20:31:29.510 ERROR 26412 --- [ctor-http-nio-2] a.w.r.e.AbstractErrorWebExceptionHandler : [c04278db-1] 500 Server Error for HTTP GET "/users/d6535843-effe-4eb7-b9ff-1689420921a3" feign.FeignException$NotFound: [404] during [GET] to [http://localhost:9002/users/d6535843-effe-4eb7-b9ff-1689420921a3] [UserServiceClient#findById(UUID)]: []
As you can see, the original API I was calling resulted in a 500 Internal Server Error
because the downstream user-service
was responding with a 404
. Not good.
So what can I do if I want to translate this 404
response into a UserNotFoundException
within the service? And what if I’d like to do something else for another method within the same client?
Well, let’s create a generic ErrorDecoder
that can defer the error handling to other classes, similarly how we do with a @ControllerAdvice
and @ExceptionHandler
class in the Sprint MVC world.
I’m going to use a custom annotation to mark methods or the client class which needs special treatment on its error handling:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface HandleFeignError { Class<? extends FeignHttpExceptionHandler> value(); }
FeignHttpExceptionHandler
is a simple interface with a single method:
public interface FeignHttpExceptionHandler { Exception handle(Response response); }
The usage is going to look the following:
@FeignClient(name = "user-service", url = "http://localhost:9002") public interface UserServiceClient { @GetMapping("/users/{id}") @HandleFeignError(UserServiceClientExceptionHandler.class) UserResponse findById(@PathVariable("id") UUID id) throws UserNotFoundException; }
The implementation for UserServiceClientExceptionHandler
is very simple:
@Component public class UserServiceClientExceptionHandler implements FeignHttpExceptionHandler { @Override public Exception handle(Response response) { HttpStatus httpStatus = HttpStatus.resolve(response.status()); String body = FeignUtils.readBody(response.body()); if (HttpStatus.NOT_FOUND.equals(httpStatus)) { return new UserNotFoundException(body); } return new RuntimeException(body); } }
Of course you can make it more sophisticated, this is just an example.
So how does the annotation work? As I said, we are going to use a special ErrorDecoder
. First of all, we have to understand the signature of the ErrorDecoder
interface. Since there’s no information within the decoder which client’s which method was called, somehow we have to figure it out so we can invoke the corresponding error handler.
One way to do it is to utilize the methodKey
parameter and build a map based on that with the error handlers. But before that, we need to somehow get a reference to all the Feign clients registered within the application:
@Component @RequiredArgsConstructor public class ExceptionHandlingFeignErrorDecoder implements ErrorDecoder { private final ApplicationContext applicationContext; private final Map<String, FeignHttpExceptionHandler> exceptionHandlerMap = new HashMap<>(); @EventListener public void onApplicationEvent(ContextRefreshedEvent event) { Map<String, Object> feignClients = applicationContext.getBeansWithAnnotation(FeignClient.class); List<Method> clientMethods = feignClients.values().stream() .map(Object::getClass) .map(aClass -> aClass.getInterfaces()[0]) .map(ReflectionUtils::getDeclaredMethods) .flatMap(Arrays::stream) .collect(Collectors.toList()); for (Method m : clientMethods) { String configKey = Feign.configKey(m.getDeclaringClass(), m); HandleFeignError handlerAnnotation = getHandleFeignErrorAnnotation(m); if (handlerAnnotation != null) { FeignHttpExceptionHandler handler = applicationContext.getBean(handlerAnnotation.value()); exceptionHandlerMap.put(configKey, handler); } } } private HandleFeignError getHandleFeignErrorAnnotation(Method m) { HandleFeignError result = m.getAnnotation(HandleFeignError.class); if (result == null) { result = m.getDeclaringClass().getAnnotation(HandleFeignError.class); } return result; } }
First off, it’s loading all the Spring beans with the @FeignClient
annotation. Since Feign is based on interfaces, there are JDK proxies involved, that’s why we need to call aClass.getInterfaces()[0]
to get the actual interface with its methods.
Then the only trick is to calculate the methodKey
which I’m doing using the Feign.configKey
method call (that’s the one Feign is also using). And as well we’re searching for the HandleFeignError
annotation on method and on class level in the same order.
So as soon as the Spring context is set up, we’ll have a map of methodKeys to actual exception handlers.
The second thing we need to do is to make sure this class implements the ErrorDecoder
interface.
@Component @RequiredArgsConstructor public class ExceptionHandlingFeignErrorDecoder implements ErrorDecoder { private final ErrorDecoder.Default defaultDecoder = new Default(); // rest is omitted for simplicity @Override public Exception decode(String methodKey, Response response) { FeignHttpExceptionHandler handler = exceptionHandlerMap.get(methodKey); if (handler != null) { return handler.handle(response); } return defaultDecoder.decode(methodKey, response); } }
So the decode method is very simple. If the map contains the methodKey
with its corresponding exception handler, we’ll use that for resolving the proper Exception
, otherwise the solution is falling back to the Default ErrorDecoder
.
Using the Feign client afterwards is quite easy:
try { UserResponse userResponse = userServiceClient.findById(id); // do something with the UserResponse } catch (UserNotFoundException e) { // Oops, the user is not found in the system // let's do some error handling }
Conclusion
With this solution in place, you can easily define proper error handling on the level of Feign client methods with any custom logic/custom exceptions you want to use. It allows you to build a more maintainable and robust application.
As usual, if you are interested in more, follow me on Twitter for updates.
UPDATE: the code can be found on my GitHub here.
Annotation Error Decoder
This module allows to annotate Feign’s interfaces with annotations to generate Exceptions based on error codes
To use AnnotationErrorDecoder with Feign, add the Annotation Error Decoder module to your classpath. Then, configure
Feign to use the AnnotationErrorDecoder:
GitHub github = Feign.builder() .errorDecoder( AnnotationErrorDecoder.builderFor(GitHub.class).build() ) .target(GitHub.class, "https://api.github.com");
Leveraging the annotations and priority order
For annotation decoding to work, the class must be annotated with @ErrorHandling
tags.
The tags are valid in both the class level as well as method level. They will be treated from ‘most specific’ to
‘least specific’ in the following order:
- A code specific exception defined on the method
- A code specific exception defined on the class
- The default exception of the method
- The default exception of the class
@ErrorHandling(codeSpecific = { @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class), @ErrorCodes( codes = {403}, generate = ForbiddenException.class), @ErrorCodes( codes = {404}, generate = UnknownItemException.class), }, defaultException = ClassLevelDefaultException.class ) interface GitHub { @ErrorHandling(codeSpecific = { @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class), @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class), }, defaultException = FailedToGetContributorsException.class ) @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); }
In the above example, error responses to ‘contributors’ would hence be mapped as follows by status codes:
Code | Exception | Reason |
---|---|---|
401 | UnAuthorizedException |
from Class definition |
403 | ForbiddenException |
from Class definition |
404 | NonExistenRepoException |
from Method definition, note that the class generic exception won’t be thrown here |
502,503,504 | RetryAfterCertainTimeException |
from method definition. Note that you can have multiple error codes generate the same type of exception |
Any Other | FailedToGetContributorsException |
from Method default |
For a class level default exception to be thrown, the method must not have a defaultException
defined, nor must the error code
be mapped at either the method or class level.
If the return code cannot be mapped to any code and no default exceptions have been configured, then the decoder will
drop to a default decoder (by default, the standard one provided by feign). You can change the default drop-into decoder
as follows:
GitHub github = Feign.builder() .errorDecoder( AnnotationErrorDecoder.builderFor(GitHub.class) .withDefaultDecoder(new MyOtherErrorDecoder()) .build() ) .target(GitHub.class, "https://api.github.com");
Complex Exceptions
Any exception can be used if they have a default constructor:
class DefaultConstructorException extends Exception {}
However, if you want to have parameters (such as the feign.Request object or response body or response headers), you have to annotate its
constructor appropriately (the body annotation is optional, provided there aren’t paramters which will clash)
All the following examples are valid exceptions:
class JustBody extends Exception { @FeignExceptionConstructor public JustBody(String body) { } } class JustRequest extends Exception { @FeignExceptionConstructor public JustRequest(Request request) { } } class RequestAndResponseBody extends Exception { @FeignExceptionConstructor public RequestAndResponseBody(Request request, String body) { } } //Headers must be of type Map<String, Collection<String>> class BodyAndHeaders extends Exception { @FeignExceptionConstructor public BodyAndHeaders(@ResponseBody String body, @ResponseHeaders Map<String, Collection<String>> headers) { } } class RequestAndResponseBodyAndHeaders extends Exception { @FeignExceptionConstructor public RequestAndResponseBodyAndHeaders(Request request, @ResponseBody String body, @ResponseHeaders Map<String, Collection<String>> headers) { } } class JustHeaders extends Exception { @FeignExceptionConstructor public JustHeaders(@ResponseHeaders Map<String, Collection<String>> headers) { } }
If you want to have the body decoded, you’ll need to pass a decoder at construction time (just as for normal responses):
GitHub github = Feign.builder() .errorDecoder( AnnotationErrorDecoder.builderFor(GitHub.class) .withResponseBodyDecoder(new JacksonDecoder()) .build() ) .target(GitHub.class, "https://api.github.com");
This will enable you to create exceptions where the body is a complex pojo:
class ComplexPojoException extends Exception { @FeignExceptionConstructor public ComplexPojoException(GithubExceptionResponse body) { if (body != null) { // extract data } else { // fallback code } } } //The pojo can then be anything you'd like provided the decoder can manage it class GithubExceptionResponse { public String message; public int githubCode; public List<String> urlsForHelp; }
It’s worth noting that at setup/startup time, the generators are checked with a null value of the body.
If you don’t do the null-checker, you’ll get an NPE and startup will fail.
Inheriting from other interface definitions
You can create a client interface that inherits from a different one. However, there are some limitations that
you should be aware of (for most cases, these shouldn’t be an issue):
- The inheritance is not natural java inheritance of annotations — as these don’t work on interfaces
- Instead, the error looks at the class and if it finds the
@ErrorHandling
annotation, it uses that one. - If not, it will look at all the interfaces the main interface
extends
— but it does so in the order the
java API gives it — so order is not guaranteed. - If it finds the annotation in one of those parents, it uses that definition, without looking at any other
- That means that if more than one interface was extended which contained the
@ErrorHandling
annotation, we can’t
really guarantee which one of the parents will be selected and you should really do handling at the child interface- so far, the java API seems to return in order of definition after the
extends
, but it’s a really bad practice
if you have to depend on that… so our suggestion: don’t.
- so far, the java API seems to return in order of definition after the
That means that as long as you only ever extend from a base interface (where you may decide that all 404’s are «NotFoundException», for example)
then you should be ok. But if you get complex in polymorphism, all bets are off — so don’t go crazy!
Example:
In the following code:
- The base
FeignClientBase
interface defines a default set of exceptions at class level - the
GitHub1
andGitHub2
interfaces will inherit the class-level error handling, which means that
any 401/403/404 will be handled correctly (provided the method doesn’t specify a more specific exception) - the
GitHub3
interface however, by defining its own error handling, will handle all 401’s, but not the
403/404’s since there’s no merging/etc (not really in the plan to implement either…)
@ErrorHandling(codeSpecific = { @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class), @ErrorCodes( codes = {403}, generate = ForbiddenException.class), @ErrorCodes( codes = {404}, generate = UnknownItemException.class), }, defaultException = ClassLevelDefaultException.class ) interface FeignClientBase {} interface GitHub1 extends FeignClientBase { @ErrorHandling(codeSpecific = { @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class), @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class), }, defaultException = FailedToGetContributorsException.class ) @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); } interface GitHub2 extends FeignClientBase { @ErrorHandling(codeSpecific = { @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class), @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class), }, defaultException = FailedToGetContributorsException.class ) @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); } @ErrorHandling(codeSpecific = { @ErrorCodes( codes = {401}, generate = UnAuthorizedException.class) }, defaultException = ClassLevelDefaultException.class ) interface GitHub3 extends FeignClientBase { @ErrorHandling(codeSpecific = { @ErrorCodes( codes = {404}, generate = NonExistentRepoException.class), @ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class), }, defaultException = FailedToGetContributorsException.class ) @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); }
Feign is a declarative web service client.
It makes writing web service clients easier.
To use Feign create an interface and annotate it.
It has pluggable annotation support including Feign annotations and JAX-RS annotations.
Feign also supports pluggable encoders and decoders.
Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters
used by default in Spring Web.
Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
1.1. How to Include Feign
To include Feign in your project use the starter with group org.springframework.cloud
and artifact id spring-cloud-starter-openfeign
. See the Spring Cloud Project page
for details on setting up your build system with the current Spring Cloud Release Train.
Example spring boot app
@SpringBootApplication
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
StoreClient.java
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
@RequestMapping(method = RequestMethod.GET, value = "/stores")
Page<Store> getStores(Pageable pageable);
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
@RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\d+}")
void delete(@PathVariable Long storeId);
}
In the @FeignClient
annotation the String value («stores» above) is an arbitrary client name, which is used to create a Spring Cloud LoadBalancer client.
You can also specify a URL using the url
attribute
(absolute value or just a hostname). The name of the bean in the
application context is the fully qualified name of the interface.
To specify your own alias value you can use the qualifiers
value
of the @FeignClient
annotation.
The load-balancer client above will want to discover the physical addresses
for the «stores» service. If your application is a Eureka client then
it will resolve the service in the Eureka service registry. If you
don’t want to use Eureka, you can configure a list of servers
in your external configuration using SimpleDiscoveryClient
.
Spring Cloud OpenFeign supports all the features available for the blocking mode of Spring Cloud LoadBalancer. You can read more about them in the project documentation.
To use @EnableFeignClients annotation on @Configuration -annotated-classes, make sure to specify where the clients are located, for example:@EnableFeignClients(basePackages = "com.example.clients") or list them explicitly: @EnableFeignClients(clients = InventoryServiceFeignClient.class)
|
1.1.1. Attribute resolution mode
While creating Feign
client beans, we resolve the values passed via the @FeignClient
annotation. As of 4.x
, the values are being resolved eagerly. This is a good solution for most use-cases, and it also allows for AOT support.
If you need the attributes to be resolved lazily, set the spring.cloud.openfeign.lazy-attributes-resolution
property value to true
.
For Spring Cloud Contract test integration, lazy attribute resolution should be used. |
1.2. Overriding Feign Defaults
A central concept in Spring Cloud’s Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the @FeignClient
annotation. Spring Cloud creates a new ensemble as an
ApplicationContext
on demand for each named client using FeignClientsConfiguration
. This contains (amongst other things) an feign.Decoder
, a feign.Encoder
, and a feign.Contract
.
It is possible to override the name of that ensemble by using the contextId
attribute of the @FeignClient
annotation.
Spring Cloud lets you take full control of the feign client by declaring additional configuration (on top of the FeignClientsConfiguration
) using @FeignClient
. Example:
@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
//..
}
In this case the client is composed from the components already in FeignClientsConfiguration
together with any in FooConfiguration
(where the latter will override the former).
FooConfiguration does not need to be annotated with @Configuration . However, if it is, then take care to exclude it from any @ComponentScan that would otherwise include this configuration as it will become the default source for feign.Decoder , feign.Encoder , feign.Contract , etc., when specified. This can be avoided by putting it in a separate, non-overlapping package from any @ComponentScan or @SpringBootApplication , or it can be explicitly excluded in @ComponentScan .
|
Using contextId attribute of the @FeignClient annotation in addition to changing the name ofthe ApplicationContext ensemble, it will override the alias of the client nameand it will be used as part of the name of the configuration bean created for that client. |
Previously, using the url attribute, did not require the name attribute. Using name is now required.
|
Placeholders are supported in the name
and url
attributes.
@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
//..
}
Spring Cloud OpenFeign provides the following beans by default for feign (BeanType
beanName: ClassName
):
-
Decoder
feignDecoder:ResponseEntityDecoder
(which wraps aSpringDecoder
) -
Encoder
feignEncoder:SpringEncoder
-
Logger
feignLogger:Slf4jLogger
-
MicrometerObservationCapability
micrometerObservationCapability: Iffeign-micrometer
is on the classpath andObservationRegistry
is available -
MicrometerCapability
micrometerCapability: Iffeign-micrometer
is on the classpath,MeterRegistry
is available andObservationRegistry
is not available -
CachingCapability
cachingCapability: If@EnableCaching
annotation is used. Can be disabled viaspring.cloud.openfeign.cache.enabled
. -
Contract
feignContract:SpringMvcContract
-
Feign.Builder
feignBuilder:FeignCircuitBreaker.Builder
-
Client
feignClient: If Spring Cloud LoadBalancer is on the classpath,FeignBlockingLoadBalancerClient
is used.
If none of them is on the classpath, the default feign client is used.
spring-cloud-starter-openfeign supports spring-cloud-starter-loadbalancer . However, as is an optional dependency, you need to make sure it been added to your project if you want to use it.
|
The OkHttpClient and Apache HttpClient 5 Feign clients can be used by setting spring.cloud.openfeign.okhttp.enabled
or spring.cloud.openfeign.httpclient.hc5.enabled
to true
, respectively, and having them on the classpath.
You can customize the HTTP client used by providing a bean of either org.apache.hc.client5.http.impl.classic.CloseableHttpClient
when using Apache HC5.
You can further customise http clients by setting values in the spring.cloud.openfeign.httpclient.xxx
properties. The ones prefixed just with httpclient
will work for all the clients, the ones prefixed with httpclient.hc5
to Apache HttpClient 5 and the ones prefixed with httpclient.okhttp
to OkHttpClient. You can find a full list of properties you can customise in the appendix.
Starting with Spring Cloud OpenFeign 4, the Feign Apache HttpClient 4 is no longer supported. We suggest using Apache HttpClient 5 instead. |
Spring Cloud OpenFeign does not provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client:
-
Logger.Level
-
Retryer
-
ErrorDecoder
-
Request.Options
-
Collection<RequestInterceptor>
-
SetterFactory
-
QueryMapEncoder
-
Capability
(MicrometerObservationCapability
andCachingCapability
are provided by default)
A bean of Retryer.NEVER_RETRY
with the type Retryer
is created by default, which will disable retrying.
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
treating them as transient network related exceptions, and any RetryableException thrown from an ErrorDecoder.
Creating a bean of one of those type and placing it in a @FeignClient
configuration (such as FooConfiguration
above) allows you to override each one of the beans described. Example:
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
}
This replaces the SpringMvcContract
with feign.Contract.Default
and adds a RequestInterceptor
to the collection of RequestInterceptor
.
@FeignClient
also can be configured using configuration properties.
application.yml
spring:
cloud:
openfeign:
client:
config:
feignName:
url: http://remote-service.com
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
responseInterceptor: com.example.BazResponseInterceptor
dismiss404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:
- com.example.FooCapability
- com.example.BarCapability
queryMapEncoder: com.example.SimpleQueryMapEncoder
micrometer.enabled: false
Default configurations can be specified in the @EnableFeignClients
attribute defaultConfiguration
in a similar manner as described above. The difference is that this configuration will apply to all feign clients.
If you prefer using configuration properties to configure all @FeignClient
, you can create configuration properties with default
feign name.
You can use spring.cloud.openfeign.client.config.feignName.defaultQueryParameters
and spring.cloud.openfeign.client.config.feignName.defaultRequestHeaders
to specify query parameters and headers that will be sent with every request of the client named feignName
.
application.yml
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
If we create both @Configuration
bean and configuration properties, configuration properties will win.
It will override @Configuration
values. But if you want to change the priority to @Configuration
,
you can change spring.cloud.openfeign.client.default-to-properties
to false
.
If we want to create multiple feign clients with the same name or url
so that they would point to the same server but each with a different custom configuration then
we have to use contextId
attribute of the @FeignClient
in order to avoid name
collision of these configuration beans.
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
public interface FooClient {
//..
}
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
public interface BarClient {
//..
}
It is also possible to configure FeignClient not to inherit beans from the parent context.
You can do this by overriding the inheritParentConfiguration()
in a FeignClientConfigurer
bean to return false
:
@Configuration
public class CustomConfiguration{
@Bean
public FeignClientConfigurer feignClientConfigurer() {
return new FeignClientConfigurer() {
@Override
public boolean inheritParentConfiguration() {
return false;
}
};
}
}
By default, Feign clients do not encode slash / characters. You can change this behaviour, by setting the value of spring.cloud.openfeign.client.decodeSlash to false .
|
1.2.1. SpringEncoder
configuration
In the SpringEncoder
that we provide, we set null
charset for binary content types and UTF-8
for all the other ones.
You can modify this behaviour to derive the charset from the Content-Type
header charset instead by setting the value of spring.cloud.openfeign.encoder.charset-from-content-type
to true
.
1.3. Timeout Handling
We can configure timeouts on both the default and the named client. OpenFeign works with two timeout parameters:
-
connectTimeout
prevents blocking the caller due to the long server processing time. -
readTimeout
is applied from the time of connection establishment and is triggered when returning the response takes too long.
In case the server is not running or available a packet results in connection refused. The communication ends either with an error message or in a fallback. This can happen before the connectTimeout if it is set very low. The time taken to perform a lookup and to receive such a packet causes a significant part of this delay. It is subject to change based on the remote host that involves a DNS lookup.
|
1.4. Creating Feign Clients Manually
In some cases it might be necessary to customize your Feign Clients in a way that is not
possible using the methods above. In this case you can create Clients using the
Feign Builder API. Below is an example
which creates two Feign Clients with the same interface but configures each one with
a separate request interceptor.
@Import(FeignClientsConfiguration.class)
class FooController {
private FooClient fooClient;
private FooClient adminClient;
@Autowired
public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerObservationCapability micrometerObservationCapability) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerObservationCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "https://PROD-SVC");
this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerObservationCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "https://PROD-SVC");
}
}
In the above example FeignClientsConfiguration.class is the default configurationprovided by Spring Cloud OpenFeign. |
PROD-SVC is the name of the service the Clients will be making requests to.
|
The Feign Contract object defines what annotations and values are valid on interfaces. Theautowired Contract bean provides supports for SpringMVC annotations, instead ofthe default Feign native annotations. |
You can also use the Builder`to configure FeignClient not to inherit beans from the parent context.
on the
You can do this by overriding calling `inheritParentContext(false)Builder
.
1.5. Feign Spring Cloud CircuitBreaker Support
If Spring Cloud CircuitBreaker is on the classpath and spring.cloud.openfeign.circuitbreaker.enabled=true
, Feign will wrap all methods with a circuit breaker.
To disable Spring Cloud CircuitBreaker support on a per-client basis create a vanilla Feign.Builder
with the «prototype» scope, e.g.:
@Configuration
public class FooConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}
The circuit breaker name follows this pattern <feignClientClassName>#<calledMethod>(<parameterTypes>)
. When calling a @FeignClient
with FooClient
interface and the called interface method that has no parameters is bar
then the circuit breaker name will be FooClient#bar()
.
As of 2020.0.2, the circuit breaker name pattern has changed from <feignClientName>_<calledMethod> .Using CircuitBreakerNameResolver introduced in 2020.0.4, circuit breaker names can retain the old pattern.
|
Providing a bean of CircuitBreakerNameResolver
, you can change the circuit breaker name pattern.
@Configuration
public class FooConfiguration {
@Bean
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
return (String feignClientName, Target<?> target, Method method) -> feignClientName + "_" + method.getName();
}
}
To enable Spring Cloud CircuitBreaker group set the spring.cloud.openfeign.circuitbreaker.group.enabled
property to true
(by default false
).
1.6. Configuring CircuitBreakers With Configuration Properties
You can configure CircuitBreakers via configuration properties.
For example, if you had this Feign client
@FeignClient(url = "http://localhost:8080")
public interface DemoClient {
@GetMapping("demo")
String getDemo();
}
You could configure it using configuration properties by doing the following
feign:
circuitbreaker:
enabled: true
alphanumeric-ids:
enabled: true
resilience4j:
circuitbreaker:
instances:
DemoClientgetDemo:
minimumNumberOfCalls: 69
timelimiter:
instances:
DemoClientgetDemo:
timeoutDuration: 10s
If you want to switch back to the circuit breaker names used prior to Spring Cloud 2022.0.0 you can set spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled to false .
|
1.7. Feign Spring Cloud CircuitBreaker Fallbacks
Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when the circuit is open or there is an error. To enable fallbacks for a given @FeignClient
set the fallback
attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();
}
@Component
static class Fallback implements TestClient {
@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}
@Override
public String getException() {
return "Fixed response";
}
}
If one needs access to the cause that made the fallback trigger, one can use the fallbackFactory
attribute inside @FeignClient
.
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
fallbackFactory = TestFallbackFactory.class)
protected interface TestClientWithFactory {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();
}
@Component
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {
@Override
public FallbackWithFactory create(Throwable cause) {
return new FallbackWithFactory();
}
}
static class FallbackWithFactory implements TestClientWithFactory {
@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}
@Override
public String getException() {
return "Fixed response";
}
}
1.8. Feign and @Primary
When using Feign with Spring Cloud CircuitBreaker fallbacks, there are multiple beans in the ApplicationContext
of the same type. This will cause @Autowired
to not work because there isn’t exactly one bean, or one marked as primary. To work around this, Spring Cloud OpenFeign marks all Feign instances as @Primary
, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the primary
attribute of @FeignClient
to false.
@FeignClient(name = "hello", primary = false)
public interface HelloClient {
// methods here
}
1.9. Feign Inheritance Support
Feign supports boilerplate apis via single-inheritance interfaces.
This allows grouping common operations into convenient base interfaces.
UserService.java
public interface UserService {
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}
UserResource.java
@RestController
public class UserResource implements UserService {
}
UserClient.java
package project.user;
@FeignClient("users")
public interface UserClient extends UserService {
}
@FeignClient interfaces should not be shared between server and client and annotating @FeignClient interfaces with @RequestMapping on class level is no longer supported.
|
1.10. Feign request/response compression
You may consider enabling the request or response GZIP compression for your
Feign requests. You can do this by enabling one of the properties:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.response.enabled=true
Feign request compression gives you settings similar to what you may set for your web server:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json
spring.cloud.openfeign.compression.request.min-request-size=2048
These properties allow you to be selective about the compressed media types and minimum request threshold length.
1.11. Feign logging
A logger is created for each Feign client created. By default the name of the logger is the full class name of the interface used to create the Feign client. Feign logging only responds to the DEBUG
level.
application.yml
logging.level.project.user.UserClient: DEBUG
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.
For example, the following would set the Logger.Level
to FULL
:
@Configuration
public class FooConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
1.12. Feign Capability support
The Feign capabilities expose core Feign components so that these components can be modified. For example, the capabilities can take the Client
, decorate it, and give the decorated instance back to Feign.
The support for Micrometer is a good real-life example for this. See Micrometer Support.
Creating one or more Capability
beans and placing them in a @FeignClient
configuration lets you register them and modify the behavior of the involved client.
@Configuration
public class FooConfiguration {
@Bean
Capability customCapability() {
return new CustomCapability();
}
}
1.13. Micrometer Support
If all of the following conditions are true, a MicrometerObservationCapability
bean is created and registered so that your Feign client is observable by Micrometer:
-
feign-micrometer
is on the classpath -
A
ObservationRegistry
bean is available -
feign micrometer properties are set to
true
(by default)-
spring.cloud.openfeign.micrometer.enabled=true
(for all clients) -
spring.cloud.openfeign.client.config.feignName.micrometer.enabled=true
(for a single client)
-
If your application already uses Micrometer, enabling this feature is as simple as putting feign-micrometer onto your classpath.
|
You can also disable the feature by either:
-
excluding
feign-micrometer
from your classpath -
setting one of the feign micrometer properties to
false
-
spring.cloud.openfeign.micrometer.enabled=false
-
spring.cloud.openfeign.client.config.feignName.micrometer.enabled=false
-
spring.cloud.openfeign.micrometer.enabled=false disables Micrometer support for all Feign clients regardless of the value of the client-level flags: spring.cloud.openfeign.client.config.feignName.micrometer.enabled .If you want to enable or disable Micrometer support per client, don’t set spring.cloud.openfeign.micrometer.enabled and use spring.cloud.openfeign.client.config.feignName.micrometer.enabled .
|
You can also customize the MicrometerObservationCapability
by registering your own bean:
@Configuration
public class FooConfiguration {
@Bean
public MicrometerObservationCapability micrometerObservationCapability(ObservationRegistry registry) {
return new MicrometerObservationCapability(registry);
}
}
It is still possible to use MicrometerCapability
with Feign (metrics-only support), you need to disable Micrometer support (spring.cloud.openfeign.micrometer.enabled=false
) and create a MicrometerCapability
bean:
@Configuration
public class FooConfiguration {
@Bean
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
return new MicrometerCapability(meterRegistry);
}
}
1.14. Feign Caching
If @EnableCaching
annotation is used, a CachingCapability
bean is created and registered so that your Feign client recognizes @Cache*
annotations on its interface:
public interface DemoClient {
@GetMapping("/demo/{filterParam}")
@Cacheable(cacheNames = "demo-cache", key = "#keyParam")
String demoEndpoint(String keyParam, @PathVariable String filterParam);
}
You can also disable the feature via property spring.cloud.openfeign.cache.enabled=false
.
1.15. Feign @QueryMap support
Spring Cloud OpenFeign provides an equivalent @SpringQueryMap
annotation, which
is used to annotate a POJO or Map parameter as a query parameter map.
For example, the Params
class defines parameters param1
and param2
:
// Params.java
public class Params {
private String param1;
private String param2;
// [Getters and setters omitted for brevity]
}
The following feign client uses the Params
class by using the @SpringQueryMap
annotation:
@FeignClient("demo")
public interface DemoTemplate {
@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
}
If you need more control over the generated query parameter map, you can implement a custom QueryMapEncoder
bean.
1.16. HATEOAS support
If your project use the org.springframework.boot:spring-boot-starter-hateoas
starter
or the org.springframework.boot:spring-boot-starter-data-rest
starter, Feign HATEOAS support is enabled by default.
When HATEOAS support is enabled, Feign clients are allowed to serialize
and deserialize HATEOAS representation models: EntityModel, CollectionModel and PagedModel.
@FeignClient("demo")
public interface DemoTemplate {
@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
}
1.17. Spring @MatrixVariable Support
Spring Cloud OpenFeign provides support for the Spring @MatrixVariable
annotation.
If a map is passed as the method argument, the @MatrixVariable
path segment is created by joining key-value pairs from the map with a =
.
If a different object is passed, either the name
provided in the @MatrixVariable
annotation (if defined) or the annotated variable name is
joined with the provided method argument using =
.
- IMPORTANT
-
Even though, on the server side, Spring does not require the users to name the path segment placeholder same as the matrix variable name, since it would be too ambiguous on the client side, Spring Cloud OpenFeign requires that you add a path segment placeholder with a name matching either the
name
provided in the@MatrixVariable
annotation (if defined) or the annotated variable name.
For example:
@GetMapping("/objects/links/{matrixVars}")
Map<String, List<String>> getObjects(@MatrixVariable Map<String, List<String>> matrixVars);
Note that both variable name and the path segment placeholder are called matrixVars
.
@FeignClient("demo")
public interface DemoTemplate {
@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
}
1.18. Feign CollectionFormat
support
We support feign.CollectionFormat
by providing the @CollectionFormat
annotation.
You can annotate a Feign client method (or the whole class to affect all methods) with it by passing the desired feign.CollectionFormat
as annotation value.
In the following example, the CSV
format is used instead of the default EXPLODED
to process the method.
@FeignClient(name = "demo")
protected interface PageableFeignClient {
@CollectionFormat(feign.CollectionFormat.CSV)
@GetMapping(path = "/page")
ResponseEntity performRequest(Pageable page);
}
Set the CSV format while sending Pageable as a query parameter in order for it to be encoded correctly.
|
1.19. Reactive Support
As the OpenFeign project does not currently support reactive clients, such as Spring WebClient, neither does Spring Cloud OpenFeign.We will add support for it here as soon as it becomes available in the core project.
Until that is done, we recommend using feign-reactive for Spring WebClient support.
1.19.1. Early Initialization Errors
Depending on how you are using your Feign clients you may see initialization errors when starting your application.
To work around this problem you can use an ObjectProvider
when autowiring your client.
@Autowired
ObjectProvider<TestFeignClient> testFeignClient;
1.20. Spring Data Support
If Jackson Databind and Spring Data Commons are on the classpath, converters for org.springframework.data.domain.Page
and org.springframework.data.domain.Sort
will be added automatically.
To disable this behaviour set
spring.cloud.openfeign.autoconfiguration.jackson.enabled=false
See org.springframework.cloud.openfeign.FeignAutoConfiguration.FeignJacksonConfiguration
for details.
1.21. Spring @RefreshScope
Support
If Feign client refresh is enabled, each Feign client is created with:
-
feign.Request.Options
as a refresh-scoped bean. This means properties such asconnectTimeout
andreadTimeout
can be refreshed against any Feign client instance. -
A url wrapped under
org.springframework.cloud.openfeign.RefreshableUrl
. This means the URL of Feign client, if defined
withspring.cloud.openfeign.client.config.{feignName}.url
property, can be refreshed against any Feign client instance.
You can refresh these properties through POST /actuator/refresh
.
By default, refresh behavior in Feign clients is disabled. Use the following property to enable refresh behavior:
spring.cloud.openfeign.client.refresh-enabled=true
DO NOT annotate the @FeignClient interface with the @RefreshScope annotation.
|
1.22. OAuth2 Support
OAuth2 support can be enabled by setting following flag:
spring.cloud.openfeign.oauth2.enabled=true
When the flag is set to true, and the oauth2 client context resource details are present, a bean of class OAuth2AccessTokenInterceptor
is created. Before each request, the interceptor resolves the required access token and includes it as a header.
OAuth2AccessTokenInterceptor
uses the OAuth2AuthorizedClientManager
to get OAuth2AuthorizedClient
that holds an OAuth2AccessToken
. If the user has specified an OAuth2 clientRegistrationId
using the spring.cloud.openfeign.oauth2.clientRegistrationId
property, it will be used to retrieve the token. If the token is not retrieved or the clientRegistrationId
has not been specified, the serviceId
retrieved from the url
host segment will be used.
- TIP
-
Using the
serviceId
as OAuth2 client registrationId is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-basedclientRegistrationId
is a suitable approach. - TIP
-
If you do not want to use the default setup for the
OAuth2AuthorizedClientManager
, you can just instantiate a bean of this type in your configuration.
1.23. Transform the load-balanced HTTP request
You can use the selected ServiceInstance
to transform the load-balanced HTTP Request.
For Request
, you need to implement and define LoadBalancerFeignRequestTransformer
, as follows:
@Bean
public LoadBalancerFeignRequestTransformer transformer() {
return new LoadBalancerFeignRequestTransformer() {
@Override
public Request transformRequest(Request request, ServiceInstance instance) {
Map<String, Collection<String>> headers = new HashMap<>(request.headers());
headers.put("X-ServiceId", Collections.singletonList(instance.getServiceId()));
headers.put("X-InstanceId", Collections.singletonList(instance.getInstanceId()));
return Request.create(request.httpMethod(), request.url(), headers, request.body(), request.charset(),
request.requestTemplate());
}
};
}
If multiple transformers are defined, they are applied in the order in which beans are defined.
Alternatively, you can use LoadBalancerFeignRequestTransformer.DEFAULT_ORDER
to specify the order.
X-Forwarded-Host
and X-Forwarded-Proto
support can be enabled by setting following flag:
spring.cloud.loadbalancer.x-forwarded.enabled=true
1.25. Supported Ways To Provide URL To A Feign Client
You can provide a URL to a Feign client in any of the following ways:
Case | Example | Details |
---|---|---|
The URL is provided in the |
|
The URL is resolved from the |
The URL is provided in the |
|
The URL is resolved from the |
The URL is not provided in the |
|
The URL is resolved from configuration properties, without load-balancing. If |
The URL is neither provided in the |
|
The URL is resolved from |
1.26. AOT and Native Image Support
Spring Cloud OpenFeign supports Spring AOT transformations and native images, however, only with refresh mode disabled, Feign clients refresh disabled (default setting) and lazy @FeignClient
attribute resolution disabled (default setting).
If you want to run Spring Cloud OpenFeign clients in AOT or native image modes, make sure to set spring.cloud.refresh.enabled to false .
|
If you want to run Spring Cloud OpenFeign clients in AOT or native image modes, ensure spring.cloud.openfeign.client.refresh-enabled has not been set to true .
|
If you want to run Spring Cloud OpenFeign clients in AOT or native image modes, ensure spring.cloud.openfeign.lazy-attributes-resolution has not been set to true .
|
Spring Cloud OpenFeign an openfeign integration module for spring boot. Feign is one of the best HTTP clients which we could use with Spring boot to communicate with third-party REST APIs. In this tutorial, we are going to explain how we can configure feign client inside a spring boot app to consume third party REST API.
Additionally, we are going to configure the same feign client in order to support real-world scenarios in development.
Major Topics inside this article, You can just click on the area you looking for if you need exact answer.
- Advantage of Using Feign as an HTTP Client
- Adding Required Dependencies
- Configuring Feign Client
- Send Requests Using Feign Client
- Testing With Postman
- Loading Feign Configurations From Application Properties
- Custom Configurations For Feign Client in Spring Boot
- Setting Dynamic Headers into the Feign Client
- Setting Feign Configurations Using Application Properties
- Configure Error Handling For Feign Client in Spring Boot
Advantage of Using Feign as an HTTP Client
As I discovered the main advantage in using feign for an HTTP client is that all we need to do is write an interface with pre-defined annotations and feign automatically do the stuff that needs to happen inside a REST client.
Let’s start coding,
Let’s create a fresh spring boot application using spring initializr, If you are not familiar with creating a spring boot application just use our guide on HOW TO CREATE A SPRING BOOT PROJECT. Here I’m using Lombok plugin to keep the code simple and clean. If you need to learn how we can use lombok in spring boot follow our article Guide to use Lombok In Spring Boot.
Adding Required Dependencies
In order to integrate Feign Client we need to include ‘spring-cloud-starter-openfeign’ along with ‘spring-cloud-dependencies’ into our project. In this tutorial, I’m using Gradle as a project building tool.
To do that add following dependencies into build.gradle,
implementation 'org.springframework.cloud:spring-cloud-dependencies:Hoxton.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:2.2.5.RELEASE'
Configuring Feign Client
All right, now we are ready to configure feign client inside this project. So let’s start coding to consume third party API.
Here we are using the third party fake API with pagination to consume using feign client. This API is hosted and open to consume for free. There are many API endpoints that cover all the HTTP methods.
First, we need to enable feign client inside the application by using ‘@EnableFeignClients’ annotation in the main class.
package com.javatodev.feign;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class OpenFeignIntegrationApplication {
public static void main(String[] args) {
SpringApplication.run(OpenFeignIntegrationApplication.class, args);
}
}
then we can start creating the interface which will act as the client for this API. To do that we just need to create an interface inside the project.
package com.javatodev.feign.client;
import com.javatodev.feign.rest.response.Airline;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
@FeignClient(value = "instantwebtools-api", url = "https://api.instantwebtools.net/v1/")
public interface ApiClient {
@RequestMapping(method = RequestMethod.GET, value = "/airlines")
List<Airline> readAirLines();
@RequestMapping(method = RequestMethod.GET, value = "/airlines/{airlineId}")
Airline readAirLineById(@PathVariable("airlineId") String airlineId);
}
In this example, we are going to consume the following API endpoints and it returns Airline Response.
https://api.instantwebtools.net/v1/airlines
https://api.instantwebtools.net/v1/airlines/:id
Response class for the API.
package com.javatodev.feign.rest.response;
import lombok.Data;
@Data
public class Airline {
private Long id;
private String name;
private String country;
private String logo;
private String slogan;
private String headQuaters;
private String website;
private String established;
}
Here we should set value which is the identifier for this API client and URL which is the base URL to the 3rd party API we are going to consume.
Send Requests Using Feign Client
All right now we are ready to consume this API through our application.
For the moment I’ll call the API with a simple Rest Controller. But in real world application these APIs could be called from a service or repository or anywhere you preferred inside a Spring Boot project.
package com.javatodev.feign.controller;
import com.javatodev.feign.client.ApiClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping(value = "/airlines")
@RequiredArgsConstructor
public class AirlineController {
private final ApiClient apiClient;
@GetMapping
public ResponseEntity readAirlineData (@RequestParam(required = false) String airlineId) {
if (airlineId == null) {
return ResponseEntity.ok(apiClient.readAirLines());
}
return ResponseEntity.ok(apiClient.readAirLineById(airlineId));
}
}
Testing With Postman
Loading Feign Configurations From Application Properties
There was few hard coded values which we have used in ApiClient. We can load those configurations from application.properties since those are constants in many cases and it will be easy to change whenever needed. To do that just need to add key value pair into the propeties and we could use those inside ApiClient as below.
app.feign.config.name=instantwebtools-api
app.feign.config.url=https://api.instantwebtools.net/v1/
Then we need to add these keys into the ApiClient in order to capture values from properties.
package com.javatodev.feign.client;
import com.javatodev.feign.rest.response.Airline;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
@FeignClient(value = "${app.feign.config.name}", url = "${app.feign.config.url}")
public interface ApiClient {
@RequestMapping(method = RequestMethod.GET, value = "/airlines")
List<Airline> readAirLines();
@RequestMapping(method = RequestMethod.GET, value = "/airlines/{airlineId}")
Airline readAirLineById(@PathVariable("airlineId") String airlineId);
}
Custom Configurations For Feign Client in Spring Boot
Feign support custom clients instead of default client. Eg;- OkHttp client which allows using HTTP/2. Additionally, there are multiple clients that support feign client in Spring boot to add more value additions to the feign.
Here we are going to create an additional class with @configuration to achieve overriding to default client with OKHTTP client.
package com.javatodev.feign.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import feign.Logger;
import feign.okhttp.OkHttpClient;
@Configuration
public class CustomFeignClientConfiguration {
@Bean
public OkHttpClient client() {
return new OkHttpClient();
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
Then we should add these configurations into the feign client we developed by using configuration in @FeignClient
@FeignClient(value = "${app.feign.config.name}", url = "${app.feign.config.url}", configuration = CustomFeignClientConfiguration.class)
Here I’m explaining ways to set HTTP headers to feign client. This is since in some cases we need to set dynamic headers like authentication tokens, basic auth credentials, auth identification, content type and etc, to a request we are sending to a 3rd party API.
Setting headers on top of the request
This is the most common and easiest way to set header into the feign client.
@RequestMapping(method = RequestMethod.GET, value = "/airlines")
List<Airline> readAirLines(@RequestHeader("X-Auth-Token") String token);
But if we use this pattern we should add same into the every API endpoint where it should be applicable and we need to pass every header while sending a request.
Setting global headers to the feign client
Let’s assume our 3rd party API requires the following headers to be present with every request which is coming to the resources.
- Content-Type:application/json
- Accept:application/json
- X-Auth-Code: 920d4d0b85443d98d86cb3c8c81d9eed
We can achieve this using feign.RequestInterceptor. Basically, this adds header values to every and each request going through the feign client in Spring boot. There are two ways of configuring RequestInterceptor with a spring boot application.
In here we can use the same configuration class which we written for above step to add requestinterceptor to set dynamic headers into every and each request foing through feign client in spring boot.
We just need to add below code snippet into our configuration class.
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("Accept", "application/json");
requestTemplate.header("Content-Type", "application/json");
requestTemplate.header("X-Auth-Code", "920d4d0b85443d98d86cb3c8c81d9eed");
};
}
Here in this example I’ve only used hard coded value for X-Auth-Code, But if you need to add dynamic authentication credentials or a tokens, just add it here.
Setting Feign Configurations Using Application Properties
We can define feign client properties using application.properties or yaml as well. This is since there is no need of having some configurations inside java based configurations instead of application properties. Eg:- connection time out value.
Let’s see how we can set those values using application.properties
feign.client.config.default.connect-timeout=20000
feign.client.config.default.read-timeout=20000
Here we have set global configuration for every and each feign client defined inside this spring boot project. But what if you need to set different configurations to different clients? It’s possible with feign client too.
To do that you just need to do is adding the feign client name instead of default to the configuration.
feign.client.config.instantwebtools-api.connect-timeout=20000
feign.client.config.instantwebtools-api.read-timeout=20000
Configure Error Handling For Feign Client in Spring Boot
In this case feign give us feign.codec.ErrorDecoder to capture and handle errors inside feign client. Basically you just need to write error CustomErrorDecoder which decodes any type of error happens inside feign communication.
package com.javatodev.feign.config;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FeignCustomErrorDecoder implements ErrorDecoder {
@Override public Exception decode(String methodKey, Response response) {
switch (response.status()) {
case 400:
log.error("Error in request went through feign client");
//handle exception
return new Exception("Bad Request Through Feign");
case 401:
log.error("Error in request went through feign client");
//handle exception
return new Exception("Unauthorized Request Through Feign");
case 404:
log.error("Error in request went through feign client");
//handle exception
return new Exception("Unidentified Request Through Feign");
default:
log.error("Error in request went through feign client");
//handle exception
return new Exception("Common Feign Exception");
}
}
}
Here I haven’t handled the exception, But in your case, you could create custom exceptions and handle these errors with whatever the exception you like on cases which added here.
Then, we need to introduce this error decoder into the feign configuration which we created earlier by adding following,
@Bean
public ErrorDecoder errorDecoder() { return new FeignCustomErrorDecoder();}
Reading original error message from feign error decoder
Here as what we have defined feign doesn’t give you the original error message which coming from the third party API. But we can read the original error using feign decoder as below.
package com.javatodev.feign.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.javatodev.feign.rest.response.FeignExceptionMessage;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FeignCustomErrorDecoder implements ErrorDecoder {
@Override public Exception decode(String methodKey, Response response) {
//START DECODING ORIGINAL ERROR MESSAGE
String erroMessage = null;
Reader reader = null;
//capturing error message from response body.
try {
reader = response.body().asReader(StandardCharsets.UTF_8);
String result = IOUtils.toString(reader);
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
FeignExceptionMessage exceptionMessage = mapper.readValue(result,
FeignExceptionMessage.class);
erroMessage = exceptionMessage.getMessage();
} catch (IOException e) {
log.error("IO Exception on reading exception message feign client" + e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
log.error("IO Exception on reading exception message feign client" + e);
}
}
//END DECODING ORIGINAL ERROR MESSAGE
switch (response.status()) {
case 400:
log.error("Error in request went through feign client {} ", erroMessage);
//handle exception
return new Exception("Bad Request Through Feign");
case 401:
log.error("Error in request went through feign client {} ", erroMessage);
//handle exception
return new Exception("Unauthorized Request Through Feign");
case 404:
log.error("Error in request went through feign client {} ", erroMessage);
//handle exception
return new Exception("Unidentified Request Through Feign");
default:
log.error("Error in request went through feign client {} ", erroMessage);
//handle exception
return new Exception("Common Feign Exception");
}
}
}
Conclusion
In this article we’ve discussed How to Use Feign Client in Spring Boot along with few more additional configurations, value additions to the feign client setup with spring boot like error handling, defining configurations, etc.
You can find source codes for this tutorial from our Github.