Reading HTTP Headers in Spring REST Controllers
Learn how to read HTTP headers in Spring Boot REST controllers using @RequestHeader, HttpServletRequest, and more.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Basic knowledge of Spring Boot and REST controllers
- ✓Familiarity with HTTP protocol and headers
- ✓Java 17+ and Spring Boot 3.x (examples use 3.1)
- Use
@RequestHeaderannotation to bind a specific header to a method parameter. - Use
HttpServletRequestfor full header access when you need all headers or dynamic header names. - Use
@RequestHeader Mapto get all headers as a map. - Always provide default values for optional headers to avoid 400 errors.
- Never trust header values without validation; they can be easily spoofed.
Think of HTTP headers as the envelope of a letter. The content inside is the body, but the envelope has metadata like the sender, recipient, and routing instructions. In a Spring REST controller, you need to peek at the envelope to make decisions—like checking an API key or a correlation ID. Spring gives you tools to read that envelope cleanly without tearing it open.
Every REST API lives on metadata. HTTP headers carry authentication tokens, correlation IDs, content negotiation hints, and custom business context. If you're building a Spring Boot API that needs to read these headers—and you will—you need to know the right tool for the job.
I've seen teams make a mess of this. They inject HttpServletRequest everywhere, couple controllers to the servlet API, and then wonder why unit tests are painful. Or they use @RequestHeader without defaults and get cryptic 400 errors when a header is missing. Even worse, they blindly trust header values and get burned by injection attacks.
In this article, I'll show you the three main ways to read HTTP headers in Spring REST controllers: @RequestHeader, HttpServletRequest, and ExchangeFilterFunction for WebClient. I'll tell you which one to use and when, and I'll share the gotchas that the official docs gloss over. By the end, you'll be able to read headers with confidence, handle missing or malformed values gracefully, and avoid the production fires I've seen firsthand.
Using @RequestHeader to Bind Specific Headers
The most common and cleanest way to read HTTP headers in a Spring REST controller is the @RequestHeader annotation. It binds a specific header value to a method parameter, and Spring handles the type conversion automatically.
Here's the basic usage:
```java @RestController public class PaymentController {
@PostMapping("/payments") public ResponseEntity<String> createPayment( @RequestHeader("X-Idempotency-Key") String idempotencyKey, @RequestHeader("Authorization") String authToken) { // use headers return ResponseEntity.ok("Payment processed"); } } ```
If the header is missing, Spring throws a MissingRequestHeaderException and returns a 400 Bad Request. That's fine for mandatory headers, but for optional headers you should provide a default value:
``java @RequestHeader(value = "X-Request-Id", defaultValue = "") String requestId ``
You can also use required = false to allow null, but I prefer defaultValue because it avoids null checks.
Type Conversion: Spring automatically converts the header string to common types like Integer, Long, Date, etc. If conversion fails, you get a MethodArgumentTypeMismatchException.
Multiple Values: If a header can appear multiple times, use String[] or List<String>:
``java @RequestHeader("X-Forwarded-For") String[] forwardedFor ``
All Headers as Map: To get all headers, use @RequestHeader Map<String, String> (or MultiValueMap to handle multiple values). This is useful for logging or forwarding.
When to use: Use @RequestHeader when you need one or a few specific headers. It's declarative, type-safe, and easy to test (just mock the header values).
Reading Headers with HttpServletRequest
Sometimes you need full access to the raw request, including all headers, or you need to read headers dynamically (e.g., header name comes from a variable). In those cases, inject HttpServletRequest directly.
```java @RestController public class AuditController {
@GetMapping("/audit") public String audit(HttpServletRequest request) { String userAgent = request.getHeader("User-Agent"); String auth = request.getHeader("Authorization"); // Get all header names Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); System.out.println(name + ": " + value); } return "Audit logged"; } } ```
Pros: Full flexibility, access to all headers, dynamic header names. Cons: Couples your controller to the servlet API, harder to unit test, no type conversion.
When to use: Use HttpServletRequest when you need to iterate over all headers, or when the header name is not known at compile time. For most other cases, @RequestHeader is cleaner.
Testing: To test controllers that use HttpServletRequest, you can mock the request object. Spring's MockHttpServletRequest works well.
Performance: Accessing getHeaderNames() and iterating is cheap, but be careful not to log all headers in production (especially auth tokens).
Reading Headers in WebClient (Reactive Stack)
If you're using Spring WebFlux or making outbound HTTP calls with WebClient, you read headers differently. For inbound requests, you use @RequestHeader with ServerHttpRequest; for outbound, you use ExchangeFilterFunction.
Inbound with ServerHttpRequest: ``java @GetMapping("/reactive") public Mono<String> reactive(ServerHttpRequest request) { String header = request.getHeaders().getFirst("X-Custom-Header"); return Mono.just("Header: " + header); } ``
Outbound with ExchangeFilterFunction: When you make calls to downstream services, you often need to propagate headers. The cleanest way is to use an ExchangeFilterFunction that copies headers from the incoming request to the outgoing one.
```java public class HeaderPropagationFilter implements ExchangeFilterFunction { private final ServerHttpRequest originalRequest;
public HeaderPropagationFilter(ServerHttpRequest originalRequest) { this.originalRequest = originalRequest; }
@Override public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { ClientRequest filteredRequest = ClientRequest.from(request) .headers(headers -> headers.addAll(originalRequest.getHeaders())) .build(); return next.exchange(filteredRequest); } } ```
Then apply it when building WebClient: ``java WebClient webClient = ``WebClient.builder() .filter(new HeaderPropagationFilter(exchange.getRequest())) .build();
When to use: Use this pattern when you need to propagate correlation IDs, auth tokens, or other context across microservices in a reactive chain.
What the Official Docs Won't Tell You
Spring's documentation is good, but it glosses over several gotchas that will bite you in production. Here are the ones I've learned the hard way:
1. Header Name Case Sensitivity The HTTP spec says header names are case-insensitive. Spring's @RequestHeader is indeed case-insensitive, but HttpServletRequest.getHeader() is also case-insensitive. However, if you use @RequestHeader Map<String, String>, the map keys are lowercase. So if you try to get "Content-Type" from the map, you'll get null. Always use lowercase when accessing the map.
2. Missing Headers and Default Values If you don't provide a default value and the header is missing, Spring throws MissingRequestHeaderException. This is fine for mandatory headers, but for optional ones, always use defaultValue. I've seen teams forget this and then add a global exception handler that returns a generic 400, which confuses clients. Be explicit.
3. Header Validation Never trust header values. They can contain malicious payloads (e.g., SQL injection, XSS). Always validate and sanitize. For example, if you read a header that should be a number, parse it with a try-catch and handle NumberFormatException.
4. Proxies and Load Balancers In cloud environments, proxies may strip or modify headers. For example, AWS ELB strips headers with underscores. Use hyphens instead (e.g., X-Custom-Header not X_Custom_Header). Also, if you need the client IP, use X-Forwarded-For header, but be aware it can be spoofed.
5. CORS and Custom Headers If you're making cross-origin requests with custom headers, the browser will first send a preflight OPTIONS request. Your server must respond with the appropriate CORS headers, including Access-Control-Allow-Headers. Otherwise, the browser blocks the request. Spring's @CrossOrigin annotation can handle this, but many developers forget to list custom headers.
6. Performance Impact of Reading All Headers Iterating over all headers with getHeaderNames() is cheap, but if you do it on every request and log them, you'll generate a lot of garbage and slow down your app. Be selective.
7. Thread Safety HttpServletRequest is not thread-safe. If you store it in a field of a singleton controller, you'll have race conditions. Always use method parameters or request-scoped beans.
These gotchas have caused production incidents in nearly every team I've consulted for. Don't ignore them.
Best Practices for Reading Headers in Production
After years of building and debugging Spring Boot APIs, here are the practices I swear by:
1. Use @RequestHeader for Specific Headers It's declarative, type-safe, and easy to test. Only fall back to HttpServletRequest when you need dynamic access.
2. Always Provide Defaults for Optional Headers Use defaultValue or required=false. This prevents 400 errors and makes your API more robust.
3. Validate and Sanitize Header Values Don't trust input. If a header is supposed to be a number, parse it in a try-catch. If it's a string, check for length limits and dangerous characters.
4. Centralize Header Extraction Logic If multiple controllers need the same header (e.g., correlation ID), extract it in a HandlerInterceptor or a base controller class. This avoids duplication and makes changes easier.
5. Log Header Issues at Debug Level When a header is missing or malformed, log a warning at debug level with enough context to trace the request. Don't log at error level unless it's truly exceptional.
6. Use a Filter for Mandatory Headers If a header is mandatory for all requests (e.g., tenant ID), use a Filter to check its presence early and return a 400 before it reaches the controller. This keeps your controllers clean.
7. Document Your Headers Use OpenAPI/Swagger annotations to document which headers your endpoints expect. This helps clients and reduces integration issues.
8. Test with Missing and Malformed Headers Write integration tests that send requests without optional headers, with invalid values, and with multiple values. Ensure your API handles these gracefully.
Example: Centralized Header Extraction with HandlerInterceptor
``java public class CorrelationIdInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String correlationId = request.getHeader("X-Correlation-Id"); if (correlationId == null || correlationId.isEmpty()) { correlationId = ``UUID.randomUUID().toString(); } CorrelationIdHolder.set(correlationId); response.setHeader("X-Correlation-Id", correlationId); return true; } }
Then in your controller, you can access the correlation ID via a static method or inject a request-scoped bean.
Testing Controllers That Read Headers
Testing header reading logic is straightforward with Spring's testing support. Here's how to do it with MockMvc and WebTestClient.
Testing with MockMvc (Servlet stack)
```java @WebMvcTest(PaymentController.class) class PaymentControllerTest {
@Autowired private MockMvc mockMvc;
@Test void testCreatePaymentWithHeaders() throws Exception { mockMvc.perform(post("/payments") .header("X-Idempotency-Key", "abc123") .header("X-Request-Id", "req-456") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string(containsString("abc123"))); }
@Test void testMissingOptionalHeader() throws Exception { mockMvc.perform(post("/payments") .header("X-Idempotency-Key", "abc123") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string(containsString("unknown"))); } } ```
Testing with WebTestClient (Reactive stack)
```java @WebFluxTest(ReactiveController.class) class ReactiveControllerTest {
@Autowired private WebTestClient webTestClient;
@Test void testReactiveHeader() { webTestClient.get().uri("/reactive") .header("X-Custom-Header", "myvalue") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Header: myvalue"); } } ```
- Happy path with all headers present.
- Missing optional headers (should use default).
- Missing mandatory headers (should return 400).
- Malformed header values (e.g., non-numeric when expecting a number).
- Multiple values for the same header.
Mocking HttpServletRequest: If your controller uses HttpServletRequest, you can mock it in unit tests, but I recommend using MockMvc which provides a mock request automatically.
The Missing Correlation ID That Broke Our Logging Pipeline
X-Correlation-Id header and that the logging framework would automatically pick it up.@RequestHeader("X-Correlation-Id") String correlationId without a default value. When the header was missing (e.g., from internal health checks), Spring threw a MissingRequestHeaderException, which was caught by a generic exception handler that logged the error but did not set a correlation ID. The logging framework then used an empty string, which the log aggregator ignored.@RequestHeader(value = "X-Correlation-Id", defaultValue = "unknown") String correlationId and added a Filter that ensures every request has a correlation ID, generating one if missing.- Always provide a default value for
@RequestHeaderunless the header is truly mandatory and you want a 400 error. - Use a servlet Filter or HandlerInterceptor to populate missing headers before they reach controllers.
- Log the actual header value at debug level during troubleshooting; don't assume it's present.
- Validate header values: they can be empty, malformed, or contain injection payloads.
- Centralize header extraction logic in a utility or filter to avoid repetition and inconsistency.
HttpServletRequest.getHeader() to debug raw values.curl -v -H "X-My-Header: value" http://localhost:8080/api/endpointCheck logs for MissingRequestHeaderException| File | Command / Code | Purpose |
|---|---|---|
| PaymentController.java | @RestController | Using @RequestHeader to Bind Specific Headers |
| AuditController.java | @RestController | Reading Headers with HttpServletRequest |
| HeaderPropagationFilter.java | public class HeaderPropagationFilter implements ExchangeFilterFunction { | Reading Headers in WebClient (Reactive Stack) |
| CorrelationIdInterceptor.java | public class CorrelationIdInterceptor extends HandlerInterceptorAdapter { | Best Practices for Reading Headers in Production |
| PaymentControllerTest.java | @WebMvcTest(PaymentController.class) | Testing Controllers That Read Headers |
Key takeaways
Interview Questions on This Topic
How do you read a specific HTTP header in a Spring Boot controller? Provide an example.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
6 min read · try the examples if you haven't