Home Java Reading HTTP Headers in Spring REST Controllers
Intermediate 6 min · July 14, 2026

Reading HTTP Headers in Spring REST Controllers

Learn how to read HTTP headers in Spring Boot REST controllers using @RequestHeader, HttpServletRequest, and more.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • 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)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use @RequestHeader annotation to bind a specific header to a method parameter.
  • Use HttpServletRequest for full header access when you need all headers or dynamic header names.
  • Use @RequestHeader Map to 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.
✦ Definition~90s read
What is How to Read HTTP Headers in Spring REST Controllers?

Reading HTTP headers in Spring REST controllers means extracting metadata from incoming requests using annotations like @RequestHeader or the HttpServletRequest object.

Think of HTTP headers as the envelope of a letter.
Plain-English First

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.

```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).

PaymentController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/payments")
public class PaymentController {

    @PostMapping
    public String createPayment(
            @RequestHeader("X-Idempotency-Key") String idempotencyKey,
            @RequestHeader(value = "X-Request-Id", defaultValue = "unknown") String requestId,
            @RequestHeader Map<String, String> allHeaders) {
        System.out.println("Idempotency-Key: " + idempotencyKey);
        System.out.println("Request-Id: " + requestId);
        System.out.println("All headers: " + allHeaders);
        return "Payment created with idempotency key: " + idempotencyKey;
    }
}
Output
Idempotency-Key: abc123
Request-Id: req-456
All headers: {host=localhost:8080, x-idempotency-key=abc123, x-request-id=req-456, content-type=application/json, ...}
💡Always provide default values for optional headers
📊 Production Insight
Header names are case-insensitive per HTTP spec, but Spring's @RequestHeader is also case-insensitive. However, some proxies or frameworks may modify header casing. Stick to lowercase in your code to avoid confusion.
🎯 Key Takeaway
Use @RequestHeader for specific headers with type conversion. Always provide defaults for optional headers.

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).

AuditController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@RestController
public class AuditController {

    @GetMapping("/audit")
    public String audit(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String value = request.getHeader(name);
            sb.append(name).append(": ").append(value).append("\n");
        }
        return sb.toString();
    }
}
Output
host: localhost:8080
user-agent: curl/7.68.0
accept: */*
x-custom-header: myvalue
...
⚠ Don't log all headers in production
📊 Production Insight
In Spring WebFlux, HttpServletRequest is not available. Use ServerHttpRequest instead. The pattern is similar but reactive.
🎯 Key Takeaway
Use HttpServletRequest when you need dynamic header access or all headers. But prefer @RequestHeader for specific headers to keep controllers clean.

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.

HeaderPropagationFilter.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.web.reactive.function.client.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Mono;

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);
    }
}
🔥Be selective when propagating headers
📊 Production Insight
WebClient's ExchangeFilterFunction runs per request. If you need to propagate headers from the original request, you must capture that request at the controller level and pass it to the filter. This is a common source of bugs in reactive microservices.
🎯 Key Takeaway
In reactive Spring, use ServerHttpRequest for inbound headers and ExchangeFilterFunction for outbound propagation.

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.

🎯 Key Takeaway
Be aware of case sensitivity, missing headers, validation, proxy effects, CORS, performance, and thread safety when reading headers.

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.

CorrelationIdInterceptor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

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;
    }
}
💡Centralize header extraction to avoid repetition
📊 Production Insight
When using HandlerInterceptor, remember that it runs after the request is matched to a handler but before the controller method. This is a good place to set up context for the request.
🎯 Key Takeaway
Use filters/interceptors for mandatory headers, validate input, and centralize extraction to keep controllers clean.

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"); } } ```

What to test
  • 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.

PaymentControllerTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.containsString;

@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("application/json"))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("abc123")));
    }

    @Test
    void testMissingOptionalHeaderDefaults() throws Exception {
        mockMvc.perform(post("/payments")
                .header("X-Idempotency-Key", "abc123")
                .contentType("application/json"))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("unknown")));
    }
}
🔥Test both missing and malformed headers
📊 Production Insight
When testing with MockMvc, the mock request does not simulate all real-world behaviors (e.g., proxy headers). Consider adding integration tests with TestContainers or a real server for critical header logic.
🎯 Key Takeaway
Use MockMvc or WebTestClient to test header handling. Cover missing, malformed, and multiple header scenarios.
● Production incidentPOST-MORTEMseverity: high

The Missing Correlation ID That Broke Our Logging Pipeline

Symptom
After deploying a new Spring Boot 3.1 service, the central logging dashboard showed no entries for that service, even though the service was processing requests successfully.
Assumption
The developer assumed that all incoming requests had a X-Correlation-Id header and that the logging framework would automatically pick it up.
Root cause
The controller method that extracted the correlation ID used @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.
Fix
Changed the annotation to @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.
Key lesson
  • Always provide a default value for @RequestHeader unless 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.
Production debug guideSymptom to Action4 entries
Symptom · 01
Client gets 400 Bad Request with message 'Missing request header'
Fix
Check if the header is sent correctly. Use curl -v or browser dev tools. If the header is optional, add defaultValue to @RequestHeader. If mandatory, verify client sends it.
Symptom · 02
Header value is null or empty in controller
Fix
Ensure the header name matches exactly (case-insensitive per HTTP spec, but Spring's binding is case-sensitive?). Actually, Spring's @RequestHeader is case-insensitive. Check for typos or extra spaces. Use HttpServletRequest.getHeader() to debug raw values.
Symptom · 03
Custom header not visible in controller but present in request
Fix
Check if a proxy or load balancer strips unknown headers. Some cloud providers filter headers with underscores. Use X- prefix for custom headers. Also check CORS configuration if it's a cross-origin request.
Symptom · 04
Header value contains unexpected characters or causes exception
Fix
Validate and sanitize header input. Headers can contain special characters. Use a whitelist or encoding. For numeric headers, handle NumberFormatException with a default or error response.
★ Quick Debug Cheat SheetCommon header reading issues and immediate actions
400 Bad Request: Missing header
Immediate action
Add defaultValue to @RequestHeader or ensure client sends it
Commands
curl -v -H "X-My-Header: value" http://localhost:8080/api/endpoint
Check logs for MissingRequestHeaderException
Fix now
@RequestHeader(value = "X-My-Header", defaultValue = "default") String myHeader
Header value is null in controller+
Immediate action
Log the raw header using HttpServletRequest
Commands
httpServletRequest.getHeader("X-My-Header")
Check for case sensitivity and whitespace
Fix now
Use @RequestHeader with required=false and handle null
Header not received but expected+
Immediate action
Check proxy/load balancer configuration
Commands
Check if header is in Access-Control-Expose-Headers for CORS
Use browser DevTools Network tab to verify request headers
Fix now
Add header to allowed headers in CORS config or proxy
Feature@RequestHeaderHttpServletRequestExchangeFilterFunction
Specific header bindingYesManualNo
Type conversionAutomaticManualManual
All headers accessVia MapYesNo
Dynamic header namesNoYesNo
Reactive supportYes (ServerHttpRequest)NoYes
TestabilityEasyHarderModerate
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
PaymentController.java@RestControllerUsing @RequestHeader to Bind Specific Headers
AuditController.java@RestControllerReading Headers with HttpServletRequest
HeaderPropagationFilter.javapublic class HeaderPropagationFilter implements ExchangeFilterFunction {Reading Headers in WebClient (Reactive Stack)
CorrelationIdInterceptor.javapublic class CorrelationIdInterceptor extends HandlerInterceptorAdapter {Best Practices for Reading Headers in Production
PaymentControllerTest.java@WebMvcTest(PaymentController.class)Testing Controllers That Read Headers

Key takeaways

1
Use @RequestHeader for specific headers with type conversion and defaults; use HttpServletRequest for dynamic or all-headers access.
2
Always provide default values for optional headers to avoid 400 errors.
3
Validate and sanitize header values; never trust client input.
4
Centralize header extraction with filters or interceptors to keep controllers clean.
5
Test header handling with MockMvc/WebTestClient, covering missing, malformed, and multiple values.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you read a specific HTTP header in a Spring Boot controller? Prov...
Q02SENIOR
What is the difference between @RequestHeader and HttpServletRequest for...
Q03SENIOR
How would you propagate a correlation ID header from an incoming request...
Q01 of 03JUNIOR

How do you read a specific HTTP header in a Spring Boot controller? Provide an example.

ANSWER
Use @RequestHeader annotation. Example: @RequestHeader("Authorization") String authToken. You can also provide a default value with defaultValue attribute.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
How do I read all HTTP headers in a Spring REST controller?
02
What happens if a required header is missing?
03
How do I read a header with multiple values?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

6 min read · try the examples if you haven't

Previous
Using Spring ResponseEntity to Manipulate HTTP Responses in REST APIs
91 / 121 · Spring Boot
Next
Entity to DTO Conversion for a Spring REST API: Mapping Strategies