If you’ve ever deployed Spring Boot behind a load balancer, you might be aware of issues coming from differences between the request into the load balancer and the request into your application. These requests will often have a different protocol, host, or port. If Spring Boot isn’t correctly setup it can lead to all sorts of mayhem, such as generating incorrect URLs for you application.

In this article you’ll discover how to make use of the X-Forwarded headers passed from a load balancer to your Spring Boot application, to help your application generate URLs correctly based on the originating request.

Spring Boot applications behind a load balancer

A common setup for a production Spring Boot application is to deploy it behind a load balancer. This has the benefit of providing:

  1. scalability since if you add more instances of your application then load will be spread between them
  2. security because a) the load balancer can handle HTTPS certificates and b) it provides a single public entry point into your application

Notice that in the above diagram the traffic is:

  • encrypted over HTTPS between the client and the load balancer
  • unencrypted over HTTP between the load balancer and the application

This setup is good enough for many organisations, since even though the internal traffic is unencrypted, the private nature of this network means there is minimal risk of it being intercepted by a bad actor.

So all good then? Not exactly, it turns out that in the above setup Spring Boot finds itself with a bit of an identify crisis.

Potential issues behind a load balancer

Imagine your Spring Boot application for whatever reason needs to return an absolute URL. Remember that an absolute URL is one that includes the domain, such as https://archive.tomgregory.com/category/general-coding.

Reasons for this include:

  • when an API response is returned with HATEOAS links to other APIs (Spring Boot’s /actuator API is a great example of this)
  • making links always use absolute URLs to avoid duplicate content issues for search engine crawlers
Generating absolute URLs in Spring Boot

A common way to implement the generation of absolute URLs is to use the information in the request to generate the URL, including the protocol, host, port, and path. Spring Boot provides the following mechanism to do this:

ServletUriComponentsBuilder.fromCurrentRequest_().build().toUriString();

This code will generate the full URL of the current request including path. Behind the scenes it uses the HttpServletRequest methods getScheme(), getServerName(), and getServerPort() to provide the required URL components.

Try it out

The accompanying GitHub repository contains a Spring Boot project with several endpoints we’ll use throughout this article.

Run the application with ./gradlew bootRun then run curl http://localhost:8080/absoluteURL to get the application to generate an absolute URL as described above. You’ll see this:

$ curl http://localhost:8080/absoluteURL
http://localhost:8080/absoluteURL
The issue with URL generation

If the request from the client is getting passed directly to the Spring Boot application, then the above approach is fine. If there is a load balancer in between though, the generated URL will be representative of the request that was made from the load balancer to the application, and not the request made into the load balancer itself.

To illustrate this, here’s a response that was generated from a Spring Boot application’s /actuator endpoint. The application sits behind a load balancer, and the request was made to https://forwarded-header-filter-example.tomgregory.com/actuator.

{
  "self": {
    "href": "http://forwarded-header-filter-example.tomgregory.com/actuator",
    "templated": false
  },
  "health": {
    "href": "http://forwarded-header-filter-example.tomgregory.com/actuator/health",
    "templated": false
  },
  "health-path": {
    "href": "http://forwarded-header-filter-example.tomgregory.com/actuator/health/{*path}",
    "templated": true
  }
}

See any problem? That’s right, the generated URLs have the http rather than https protocol. Anyone following these links is going to get a big dose of disappointment because they won’t work. 😈

Wouldn’t it be nice if there were a way we could use the details of the originating request to generate an absolute URL?

Introducing the X-Forwarded headers

Fortunately, a solution for this problem already exists with X-Forwarded headers. These special headers are often set by load balancers to tell downstream services where the original request came from. They include:

  • x-forwarded-proto the originating request’s protocol (HTTP/HTTPS)
  • x-forwarded-port the originating request’s port
  • x-forwarded-for the originating IP address

There are other X-Forwarded headers but we only need the first two to achieve our desired outcome. These headers are added by default to requests passing through the AWS Application Load Balancer, and other cloud providers most likely support them.

Here is a full list of headers that were passed through to the example Spring Boot application, after I deployed it into AWS behind an application load balancer. The request was made running curl https://forwarded-header-filter-example.tomgregory.com/absoluteURL.

"headers": {
  "x-amzn-trace-id": [
    "Root=1-5f800790-3994edf14fa982914c388707"
  ],
  "x-forwarded-proto": [
    "https"
  ],
  "host": [
    "forwarded-header-filter-example.tomgregory.com"
  ],
  "x-forwarded-port": [
    "443"
  ],
  "x-forwarded-for": [
    "73.59.102.103"
  ],
  "user-agent": [
    "curl/7.68.0"
  ],
  "accept": [
    "*/*"
  ]
}

Even though the request into the Spring Boot application from the load balancer was over HTTP, you can clearly see above that we have information about the originating request, including:

  • the originating protocol of https
  • the originating port of 443
  • the originating IP address of 73.59.102.103

Using X-Forwarded headers with Spring Boot

Fortunately, the clever people over at Spring HQ have a solution for this in the form of the request filter class ForwardedHeaderFilter. Remember that in Spring a request filter is a class that does some processing before the request is passed to the controller.

So what exactly does this filter do? Well, here’s what the docs say:

Extract values from “Forwarded” and “X-Forwarded-*” headers, wrap the request and response, and make they reflect the client-originated protocol and address in the following methods: getServerName() getServerPort() getScheme() isSecure() sendRedirect(String)

That’s very cool, because those are exactly the methods used to generate the absolute URL in the example application. So let’s try this out then!

Configuring the ForwardedHeaderFilter

To add the ForwardedHeaderFilter into your application you need to configure a FilterRegistrationBean bean like this:

    @Bean
    public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
        ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
        FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter);
        registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        registration.setUrlPatterns(List.of("/absoluteURLWithFilter"));
        return registration;
    }
  • we create the filter and wrap it in a filter registration bean
  • we set the dispatcher types according to this Spring documentation
  • we set the highest precedence ordering so that this filter gets applied first, before any others. This is important in case any other filters need to use the wrapped HTTP request.
  • we configure the filter to apply to only a specific URL using a URL pattern

Demo of Spring Boot behind a load balancer with X-Forwarded headers

Let’s cut to the chase and see what happens when a Spring Boot application deployed behind a load balancer tries to generate a URL with and without the ForwardedHeaderFilter. This has been tested in AWS using the below setup:

  • the Spring Boot application is deployed behind an AWS Application Load Balancer
  • access to the load balancer is over HTTPS
  • access from the load balancer to the application is over HTTP
  • there are two endpoints exposed on the application which generate an absolute URL in the same way using the ServletUriComponentsBuilder, as described earlier
    1. /absoluteURL generates the URL with no filters applied
    2. /absoluteURLWithFilter generates the URL but with the ForwardedHeaderFilter applied first

So, here are the results of this little experiment…

No filter

When I run curl https://forwarded-header-filter-example.tomgregory.com/absoluteURL I get a response of http://forwarded-header-filter-example.tomgregory.com/absoluteURL. Wrong protocol. 😢

With filter

But when I run curl https://forwarded-header-filter-example.tomgregory.com/absoluteURLWithFilter I get a response of https://forwarded-header-filter-example.tomgregory.com/absoluteURLWithFilter. Much better!

You can see that when the ForwardedHeaderFilter is applied we get the HTTPS protocol in the generated URL, which comes from the X-Forwarded-Proto header added by the load balancer. Awesome! ✅

Try it out yourself in AWS

Sadly I can’t leave this service running all the time for you to try out. Ain’t nobody got CPU for that! Instead, why not launch this CloudFormation stack into your own AWS account using the button below?

This will launch all the resources required to get this example up and running, including a VPC, subnets, load balancer, and Spring Boot application deployment using AWS Elastic Container Service (see template for full details).

Launch CloudFormation stack

You’ll need to set the CertificateArn parameter to apply this template. This is the ARN (Amazon Resource Name) of a certificate which will be used to encrypt traffic into the load balancer. You can create one by going to Services > Certificate Manager > Request a certificate.

Once the stack has launched go to Services > EC2 > Load Balancers then select the load balaner and copy the DNS name. You can then access the application on:

  • https://<load-balancer-dns-name>:443/absoluteURL
  • https://<load-balancer-dns-name>:443/absoluteURLWithFilter

You’ll have to accept the invalid certificate warnings, as the certificate doesn’t match the load balancer DNS name. If you’re using curl pass the --insecure flag.

To resolve this problem properly, the certificate used by this CloudFormation template must be created for your own domain, then a CNAME record pointing to the load balancer should be added in your domain provider’s DNS settings.

Don’t forgot to delete the CloudFormation stack once you’re finished playing to avoid incurring any unnecessary charges.

Summary

If you’re deploying a Spring Boot application behind a load balancer, then consider using Spring’s ForwardedHeaderFilter. This filter will help by making sure X-Forwarded headers passed through from the load balancer are considered, preventing problems with generated URLs having the wrong protocol or port.

Resources

Launch CloudFormation stack