Hello! Welcome back to the course.
In our last set of lessons, you did an excellent job implementing our core in-memory rate limiting algorithms and, crucially, unifying them under a common RateLimiter interface. This was a vital step in building a flexible system. That interface has a single method, isAllowed(String key), which naturally leads to our next critical question: how do we generate that key?
A robust rate limiter needs to identify clients in multiple ways—by IP address, user ID, API key, or even the specific API endpoint being called. This lesson is the first step in building that multi-tenant capability. We will explore how to access the raw information from an incoming web request, which will serve as the foundation for our key resolution strategies.
Your learning outcome for this lesson:
You will learn to extract client identifiers from HTTP headers and the request context. By the end of this lesson, you'll understand how to inspect an incoming request in a Spring Boot application to pull out the pieces of information needed to uniquely identify a client.
1. The Source of Truth: HttpServletRequest
In a Java web application built on the servlet standard, like our Spring Boot project, every incoming HTTP request is represented by an object that implements the HttpServletRequest interface. This object is a treasure trove of information, containing everything from the request URL and headers to the client's IP address.
As you'll recall from Module 1, we created a RateLimitingInterceptor. Its preHandle method is perfectly positioned to inspect the request before it reaches your controller. The preHandle method conveniently receives the HttpServletRequest as an argument.

To learn about the most common methods we'll use, please read the following sections from a Baeldung article.
Get Client Information From HTTP Request in Java
To extract information from a request, we need to understand the object that holds it. In Spring's servlet-based applications, this is the HttpServletRequest. This article from Baeldung provides a great overview of the key methods we'll need.
Please read section 2 'HTTP Request Object' and its subsections 2.1 'Getting the Client IP Address' and 2.4 'Getting Client Headers'. Focus on the methods getRemoteAddr() and getHeader(). These are the fundamental tools for our key resolvers.
As you've just read, we can access critical information using simple method calls within our interceptor:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class RateLimitingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Example: Get the client's IP address
String ipAddress = request.getRemoteAddr();
// Example: Get a custom API key header
String apiKey = request.getHeader("X-API-KEY");
// Example: Get the requested endpoint path
String requestUri = request.getRequestURI();
System.out.println("Request from IP: " + ipAddress + " with API Key: " + apiKey + " to " + requestUri);
// In future lessons, we will use this info to generate a key and check the rate limit.
// For now, we just allow the request to proceed.
return true;
}
}
These methods are the building blocks for all our key resolution strategies. However, there's a critical real-world complication with request.getRemoteAddr().
2. The Proxy Problem and the X-Forwarded-For Header
In almost any production environment, your application won't be exposed directly to the internet. It will sit behind one or more reverse proxies, load balancers (like AWS ALB or Nginx), or API gateways.
When a request passes through a proxy, the proxy terminates the original TCP connection from the client and establishes a new connection to your application server. This means that from your application's perspective, the request is coming from the proxy's IP address. If you use request.getRemoteAddr(), you'll get the IP of the load balancer, not the actual user. This would cause all users to share a single rate limit bucket—a critical failure.
To solve this, proxies add a standard HTTP header, X-Forwarded-For, which contains the IP address of the original client.
The following video provides an excellent explanation of this problem and its solution.
Understanding X-Forwarded-For header in ALB
The getRemoteAddr() method has a major limitation in modern cloud environments. This video clearly explains the problem that arises when your application is behind a load balancer and introduces the standard solution.
Watch the following segments: 00:00 - 00:57: Explains the core problem. 04:42 - 06:06: Shows how the X-Forwarded-For header is added by a load balancer and how it solves the problem. Focus on the concept of the header being added to preserve the original client IP.
To build a robust IP resolver, you must always check for the X-Forwarded-For header first and fall back to getRemoteAddr() only if it's not present.
A reliable implementation looks like this:
private String getClientIpAddress(HttpServletRequest request) {
String xffHeader = request.getHeader("X-Forwarded-For");
if (xffHeader == null || xffHeader.isEmpty()) {
// If the header is missing, fall back to the remote address.
return request.getRemoteAddr();
}
// The X-Forwarded-For header can be a comma-separated list of IPs.
// The first IP in the list is the original client's IP.
return xffHeader.split(",")[0].trim();
}
This logic is essential for any service intended to run in a modern, containerized, or cloud-native environment.
3. Applying Extraction Logic in the Interceptor
We've established that our RateLimitingInterceptor is the right place to perform this logic. While we could put all our key-generation code directly in the preHandle method, that would lead to a monolithic and inflexible design.
As we designed in Module 2, our goal is to use a RateLimitKeyResolver interface, allowing us to swap out different key-generation strategies. The interceptor's job is simply to call the configured resolver.
The following article reinforces how a HandlerInterceptor is the ideal place for this kind of cross-cutting concern.
Extracting a Custom Header From the Request
Now let's look at how to apply this knowledge specifically within a Spring HandlerInterceptor. This Baeldung article provides a perfect example of extracting a header, which is precisely the pattern we will use.
Please read section 4, 'Using HandlerInterceptor'. Pay close attention to how the preHandle method receives the HttpServletRequest and uses request.getHeader() to extract information. This demonstrates the pattern we will use to feed request data into our key resolvers.
The logic inside the interceptor's preHandle method will look something like this conceptually:
- Receive the
HttpServletRequest. - Pass the
requestobject to an activeRateLimitKeyResolverimplementation. - The resolver implements the specific logic (e.g., getting the IP, getting an API key header) and returns a
Stringkey. - The interceptor uses this key to call the
isAllowed(key)method on the appropriateRateLimiter.
This elegant separation of concerns is what will make our final engine so powerful and maintainable.
Key Takeaways
- The
HttpServletRequestobject is our primary source for client information in a Spring Boot web application. - We can access the client's IP, headers, and request path using methods like
getRemoteAddr(),getHeader(), andgetRequestURI(). - In a production environment behind a proxy or load balancer, you must check the
X-Forwarded-Forheader to get the true client IP address. - Our
RateLimitingInterceptoris the ideal component to access theHttpServletRequestand delegate the task of key generation to a specializedRateLimitKeyResolver.
Next Steps
You now have the foundational knowledge to pull any piece of data you need from an incoming request. In our next lesson, we will put this into practice by implementing our first concrete RateLimitKeyResolver: one that extracts a client's API key from a custom HTTP header.