Course – LS (cat=REST)

Get started with Spring and Spring Boot, through the reference Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Sometimes we need to set request headers in our HTTP calls when using Feign. Feign allows us to build HTTP clients simply with a declarative syntax.

In this short tutorial, we’ll see how to configure the request headers using annotations. We’ll also see how to include common request headers by using interceptors.

2. Example

Throughout this tutorial, we’ll be using an example Bookstore Application that exposes REST API endpoints. 

We can easily clone the project and run it locally:

$ mvn install spring-boot:run

Let’s deep dive into the client-side implementation.

3. Using the Header Annotation

Let’s think of a scenario where specific API calls should always contain a static header. In this situation, we might configure that request header as part of the client. A typical example is to include a Content-Type header.

Using the @Header annotation, we can easily configure a static request header. We can define this header value either statically or dynamically.

3.1. Setting Static Header Value

Let’s configure two static headers, i.e., Accept-Language and Content-Type, into the BookClient:

@Headers("Accept-Language: en-US")
public interface BookClient {
    
    @RequestLine("GET /{isbn}")
    BookResource findByIsbn(@Param("isbn") String isbn);

    @RequestLine("POST")
    @Headers("Content-Type: application/json")
    void create(Book book);
}

In the above code, the header Accept-Language is included in all APIs since it is applied to the BookClient. However, the create method has an additional Content-Type header.

Next, let’s see how to create the BookClient using Feign’s Builder method and passing the HEADERS log level:

Feign.builder()
  .encoder(new GsonEncoder())
  .decoder(new GsonDecoder())
  .logger(new Slf4jLogger(type))
  .logLevel(Logger.Level.HEADERS)
  .target(BookClient.class, "http://localhost:8081/api/books");

Now, let’s test the create method:

String isbn = UUID.randomUUID().toString();
Book book = new Book(isbn, "Me", "It's me!", null, null);
        
bookClient.create(book);

book = bookClient.findByIsbn(isbn).getBook();

Then, let’s verify the headers in the output logger:

18:01:15.039 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#create] Accept-Language: en-US
18:01:15.039 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#create] Content-Type: application/json
18:01:15.096 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#findByIsbn] Accept-Language: en-US

We should note that if the header name is the same in both the client interface and the API method, they will not override each other. Instead, the request will include all such values.

3.2. Setting Dynamic Header Value

Using the @Header annotation, we can also set a dynamic header value. For this, we need to express the value as a placeholder.

Let’s include the x-requester-id header into the BookClient with a placeholder of requester:

@Headers("x-requester-id: {requester}")
public interface BookClient {
   
    @RequestLine("GET /{isbn}")
    BookResource findByIsbn(@Param("requester") String requester, @Param("isbn") String isbn);
}

Here we made the x-requester-id a variable that’s passed into each method. We used the @Param annotation to match the name of the variable. It’s expanded at runtime to satisfy the header specified by the @Headers annotation.

Now, let’s call the BookClient API with the x-requester-id header:

String requester = "test";
book = bookClient.findByIsbn(requester, isbn).getBook();

Then, let’s verify the request header in the output logger:

18:04:27.515 [main] DEBUG c.b.f.c.h.s.parameterized.BookClient - [BookClient#findByIsbn] x-requester-id: test

4. Using the HeaderMaps Annotation

Let’s imagine a scenario where the header keys and values are all dynamic. In this situation, the range of possible keys is unknown ahead of time. Also, the headers may vary between different method calls on the same client. A typical example would be setting certain metadata headers.

Using a Map parameter annotated with @HeaderMap sets the dynamic headers:

@RequestLine("POST")
void create(@HeaderMap Map<String, Object> headers, Book book);

Now, let’s try to test the create method with the header map:

Map<String,Object> headerMap = new HashMap<>();
    	
headerMap.put("metadata-key1", "metadata-value1");
headerMap.put("metadata-key2", "metadata-value2");
    	
bookClient.create(headerMap, book);

Then, let’s verify the headers in the output logger:

18:05:03.202 [main] DEBUG c.b.f.c.h.dynamicheader.BookClient - [BookClient#create] metadata-key1: metadata-value1
18:05:03.202 [main] DEBUG c.b.f.c.h.dynamicheader.BookClient - [BookClient#create] metadata-key2: metadata-value2

5. Request Interceptors

Interceptors can perform various implicit tasks like logging or authentication for every request or response.

Feign provides a RequestInterceptor interface. With this, we can add request headers.

It makes sense to add a request interceptor when it’s known that the header should be included in every call. This pattern removes the dependency of the invoking code to implement non-functional requirements like authentication or tracing.

Let’s try this out by implementing an AuthorisationService which we’ll use to generate the authorization token:

public class ApiAuthorisationService implements AuthorisationService {

    @Override
    public String getAuthToken() {
        return "Bearer " + UUID.randomUUID();
    }
}

Now, let’s implement our custom request interceptor:

public class AuthRequestInterceptor implements RequestInterceptor {
	
    private AuthorisationService authTokenService;
   
    public AuthRequestInterceptor(AuthorisationService authTokenService) {
        this.authTokenService = authTokenService;
    }

    @Override
    public void apply(RequestTemplate template) {
        template.header("Authorisation", authTokenService.getAuthToken());
    }
}

We should note that request interceptors can read, remove or mutate any part of the request template.

Now, let’s add the AuthInterceptor to the BookClient using the builder method:

Feign.builder()
  .requestInterceptor(new AuthInterceptor(new ApiAuthorisationService()))
  .encoder(new GsonEncoder())
  .decoder(new GsonDecoder())
  .logger(new Slf4jLogger(type))
  .logLevel(Logger.Level.HEADERS)
  .target(BookClient.class, "http://localhost:8081/api/books");

Then, let’s test the BookClient API with the Authorisation header:

bookClient.findByIsbn("0151072558").getBook();

Now, let’s verify the header in the output logger:

18:06:06.135 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#findByIsbn] Authorisation: Bearer 629e0af7-513d-4385-a5ef-cb9b341cedb5

Multiple Request interceptors can also be applied to the Feign client. Though no guarantees are given concerning the order that they are applied.

6. Conclusion

In this article, we’ve discussed how Feign client supports setting request headers. We implemented that using the @Headers, @HeaderMaps annotation, and request interceptors.

As always, all code samples shown in this tutorial are available over on GitHub.

Course – LS (cat=REST)

Get started with Spring and Spring Boot, through the Learn Spring course :

>> CHECK OUT THE COURSE
res – REST (eBook) (cat=REST)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.