The @RequestHeader annotation is used to bind HTTP request header attributes values to controller method parameters.
Definition of RequestHeaderVersion: 7.0.5 package org.springframework.web.bind.annotation;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestHeader {
@AliasFor("name")
String value() default ""; 1
@AliasFor("value")
String name() default ""; 2
boolean required() default true; 3
String defaultValue() default ValueConstants.DEFAULT_NONE; 4
}
Examples
To test following example please download the project and run embedded Jetty server which has been setup in pom.xml
mvn clean compile jetty:run
Using 'value' element of @RequestHeader
@RequestHeader element 'value' is used to specify HTTP request header name:
@Controller
@RequestMapping("trades")
public class TradeController {
private static final Logger LOGGER = Logger.getLogger(TradeController.class.getName());
@RequestMapping(headers = "User-Agent")
public String handleAllTradesRequests(@RequestHeader("User-Agent") String userAgent,
Model model) {
LOGGER.info("all trades requests, User-Agent header : " + userAgent);
model.addAttribute("msg",
"all trades requests, User-Agent header : " + userAgent);
return "my-page";
}
.............
}
$ curl -s http://localhost:8080/trades -H "User-Agent: ABC"
<html> <body> <h3> Message : all trades requests, User-Agent header : ABC <h3> </body> </html>
@RequestHeader without 'value' element
Just like @PathVariable and @RequestParam, 'value' element of @RequestHeader annotation can be skipped if type variable name is same as header name. In that case the code should be compiled with debugging information.
@Controller
@RequestMapping("trades")
public class TradeController {
private static final Logger LOGGER = Logger.getLogger(TradeController.class.getName());
.............
@RequestMapping(headers = "From")
public String handleRequestByFromHeader(@RequestHeader String From,
Model model) {
LOGGER.info("trade request by From header : " + From);
model.addAttribute("msg", "trade request by From header : " + From);
return "my-page";
}
.............
}
$ curl -s http://localhost:8080/trades -H "User-agent:" -H "From: abc@gmail.com"
<html> <body> <h3> Message : trade request by From header : abc@gmail.com <h3> </body> </html>
Sometimes Header names are not java valid variable names e.g. User-Agent, in that case we cannot use this method of header binding.
Note: Curl automatically adds a User-Agent header by default to every request. So in above example we suppressed curl's default User-Agent header using the -H flag with an empty value to avoid unintentional handler mapping.
Using multiple @RequestHeader annotations
A method can have any number of @RequestHeaders annotations:
@Controller
@RequestMapping("trades")
public class TradeController {
private static final Logger LOGGER = Logger.getLogger(TradeController.class.getName());
.............
@RequestMapping(headers = {"User-Agent", "Accept-Language"})
public String handleRequestByTwoHeaders(@RequestHeader("User-Agent") String userAgent,
@RequestHeader("Accept-Language") String
acceptLanguage,
Model map) {
String msg = "Trade request by User-Agent and Accept headers : " + userAgent + ", " +
acceptLanguage;
LOGGER.info(msg);
map.addAttribute("msg", msg);
return "my-page";
}
.............
}
$ curl -s http://localhost:8080/trades -H "User-agent: Spring Test" -H "Accept-Language: fr"
<html> <body> <h3> Message : Trade request by User-Agent and Accept headers : Spring Test, fr <h3> </body> </html>
Using Map with @RequestHeader for all headers
If the method parameter is Map
or MultiValueMap
then the map is populated with all request header names and values.
@Controller
@RequestMapping("trades")
public class TradeController {
private static final Logger LOGGER = Logger.getLogger(TradeController.class.getName());
.............
@RequestMapping
public String handleRequestWithAllHeaders(@RequestHeader Map<String, String> header,
Model model) {
String msg = "Trade request with all headers : " + header;
LOGGER.info(msg);
model.addAttribute("msg", msg);
return "my-page";
}
.............
}
$ curl -s http://localhost:8080/trades -H "User-agent:" -H "CustomHeader: xyz" -H "CustomHeader2: abc"
<html> <body> <h3> Message : Trade request with all headers : {Host=localhost:8080, Accept=*/*, CustomHeader=xyz, CustomHeader2=abc} <h3> </body> </html>
We can also use method parameter of type HttpHeaders along with the annotation @RequestHeader to get all headers. HttpHeaders is Spring specific type which implements MultiValueMap<String,String> and defines all convenience methods like getContentType(), getCacheControl() etc.
@Controller
@RequestMapping("trades")
public class TradeController {
private static final Logger LOGGER = Logger.getLogger(TradeController.class.getName());
.............
@RequestMapping
public String handleRequestWithAllHeaders(@RequestHeader Map<String, String> header,
Model model) {
String msg = "Trade request with all headers : " + header;
LOGGER.info(msg);
model.addAttribute("msg", msg);
return "my-page";
}
.............
}
$ curl -s http://localhost:8080/trades/tradeCurrencies -H "User-agent: " -H "CustomHeader: abc"
<html> <body> <h3> Message : Trade request with all httpHeaders : [Host:"localhost:8080", Accept:"*/*", CustomHeader:"abc"] <h3> </body> </html>
Auto type conversion
If target method parameter is not String, automatic type conversion can happen. All simple types such as int, long, Date, etc. are supported by default.
@Controller
@RequestMapping("trades")
public class TradeController {
private static final Logger LOGGER = Logger.getLogger(TradeController.class.getName());
.............
@RequestMapping(value = "{tradeId}")
public String handleRequestById(@PathVariable("tradeId") String tradeId,
@RequestHeader("If-Modified-Since") Date date,
Model model) {
String msg = "Trade request by trade id and If-Modified-Since header : " + tradeId + ", "
+ date;
LOGGER.info(msg);
model.addAttribute("msg", msg);
return "my-page";
}
.............
}
$ curl -s http://localhost:8080/trades/23 -H "User-agent:" -H "If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT"
<html> <body> <h3> Message : Trade request by trade id and If-Modified-Since header : 23, Sun Oct 30 03:43:31 CST 1994 <h3> </body> </html>
The 'required' element of @RequestHeader
This element defines whether the header is required. The default is true. That means the status code 400 will be returned if the header is missing in the request. We can switch this to false if we prefer a null value if the header is not present in the request. Following handler will still map even though header 'Accept' is not present in the request:
@Controller
@RequestMapping("trades")
public class TradeController {
private static final Logger LOGGER = Logger.getLogger(TradeController.class.getName());
.............
@RequestMapping(value = "exchangeRates")
public String handleExchangeRatesRequest(
@RequestHeader(value = "Accept", required = false) String acceptHeader,
Model model) {
String msg = "exchange rates request. Accept header: " + acceptHeader;
LOGGER.info(msg);
model.addAttribute("msg", msg);
return "my-page";
}
}
$ curl -s http://localhost:8080/trades/exchangeRates -H "User-agent:" -H "Accept:"
<html> <body> <h3> Message : exchange rates request. Accept header: null <h3> </body> </html>
In above example we can alternatively use @RequestMapping(... produces= "application/xml") instead of mapping via @RequestHeader(value = "Accept"). In that case, however, the handler method will only match if header has Accept="application/xml". Whereas, above example will map Accept header doesn't matter what value Accept has been assigned to.
The 'defaultValue' element of @RequestHeader
The defaultValue element is used as a fallback value when the request header specified by 'value' element is not provided or has an empty value. Supplying a default value implicitly sets required to false.
In our last example we can specify a default value for 'Accept' header. In that case we don't have to specify 'required=false'. Now if the value for this header is present or not, the handler method will still be mapped.
@RequestHeader(value = "Accept", defaultValue="application/json") String acceptHeader
Same base URI methods with different request headers are ambiguous
Defining different request headers does not define different URI paths. Following handler methods will cause runtime exception, complaining about ambiguous mapping.
@Controller
@RequestMapping("/trades")
public class TradeController {
@RequestMapping
public String handleAllTradesRequests (@RequestHeader("User-Agent") String userAgent,
Model model) {
model.addAttribute("msg", "all trades requests, User-Agent header : "
+ userAgent);
return "my-page";
}
@RequestMapping
public String handleRequestByFromHeader (@RequestHeader("From") String from,
Model model) {
model.addAttribute("msg", "trade request by From header : " + from);
return "my-page";
}
}
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'myMvcController' method
public java.lang.String com.logicbig.example.TradeController.handleRequestByFromHeader(java.lang.String,org.springframework.ui.Model)
to {[/trades]}: There is already 'myMvcController' bean method
public java.lang.String com.logicbig.example.TradeController.handleAllTradesRequests(java.lang.String,org.springframework.ui.Model) mapped.
Avoid ambiguity by using @RequestMapping(headers = ....)
We can fix the ambiguity similar to @RequestParam where we used 'params' . In case of @RequestHeader we can define related headers in @RequestMapping annotation.
@Controller
@RequestMapping("trades")
public class TradesController {
@RequestMapping(headers = "User-Agent")
public String handleAllTradesRequests (@RequestHeader("User-Agent") String userAgent,
Model model) {
model.addAttribute("msg", "all trades requests, User-Agent header : "
+ userAgent);
return "my-page";
}
@RequestMapping(headers = "From")
public String handleRequestByFromHeader (@RequestHeader("From") String from,
Model model) {
model.addAttribute("msg", "trade request by From header : " + from);
return "my-page";
}
Unit Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class TradeControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
public void setup() {
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.wac);
this.mockMvc = builder.build();
}
@Test
public void testUserController() throws Exception {
ResultMatcher ok = MockMvcResultMatchers.status().isOk();
MockHttpServletRequestBuilder builder =
MockMvcRequestBuilders.get("/trades")
.header("User-Agent",
"spring test");
this.mockMvc.perform(builder)
.andExpect(ok);
builder = MockMvcRequestBuilders.get("/trades")
.header("From",
"user@example.com");
this.mockMvc.perform(builder).andExpect(ok);
builder = MockMvcRequestBuilders.get("/trades/tradeCurrencies");
this.mockMvc.perform(builder)
.andExpect(ok);
builder = MockMvcRequestBuilders.get("/trades")
.header("User-Agent", "Spring test")
.header("Accept-Language", "fr");
this.mockMvc.perform(builder)
.andExpect(ok);
builder = MockMvcRequestBuilders.get("/trades/23")
.header("If-Modified-Since",
"Sat, 29 Oct 1994 19:43:31 GMT");
this.mockMvc.perform(builder)
.andExpect(ok);
builder = MockMvcRequestBuilders.get("/trades/exchangeRates");
this.mockMvc.perform(builder)
.andExpect(ok);
this.mockMvc.perform(builder).andExpect(ok);
}
}
mvn test Output$ mvn test [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for com.logicbig.example:spring-mvc-request-header:war:1.0-SNAPSHOT [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-war-plugin is missing. @ line 38, column 21 [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed projects. [WARNING] [INFO] [INFO] -----------< com.logicbig.example:spring-mvc-request-header >----------- [INFO] Building spring-mvc-request-header 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ spring-mvc-request-header --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-mvc-request-header\src\main\resources [INFO] [INFO] --- compiler:3.5.1:compile (default-compile) @ spring-mvc-request-header --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-mvc-request-header --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-mvc-request-header\src\test\resources [INFO] [INFO] --- compiler:3.5.1:testCompile (default-testCompile) @ spring-mvc-request-header --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ spring-mvc-request-header --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.TradeControllerTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.639 s -- in com.logicbig.example.TradeControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.284 s [INFO] Finished at: 2026-03-10T16:14:41+08:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 0 ms Mar 10, 2026 4:14:41 PM com.logicbig.example.TradeController handleAllTradesRequests INFO: all trades requests, User-Agent header : spring test Mar 10, 2026 4:14:41 PM com.logicbig.example.TradeController handleRequestByFromHeader INFO: trade request by From header : user@example.com Mar 10, 2026 4:14:41 PM com.logicbig.example.TradeController handleRequestWithAllHeaders2 INFO: Trade request with all httpHeaders : [] Mar 10, 2026 4:14:41 PM com.logicbig.example.TradeController handleRequestByTwoHeaders INFO: Trade request by User-Agent and Accept headers : Spring test, fr Mar 10, 2026 4:14:41 PM com.logicbig.example.TradeController handleRequestById INFO: Trade request by trade id and If-Modified-Since header : 23, Sun Oct 30 03:43:31 CST 1994 Mar 10, 2026 4:14:41 PM com.logicbig.example.TradeController handleExchangeRatesRequest INFO: exchange rates request. Accept header: null Mar 10, 2026 4:14:41 PM com.logicbig.example.TradeController handleExchangeRatesRequest INFO: exchange rates request. Accept header: null
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.5 (Spring Web MVC)
Version Compatibility: 4.0.0.RELEASE - 7.0.5 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.5 (Spring TestContext Framework)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- junit-jupiter-engine 6.0.2 (Module "junit-jupiter-engine" of JUnit)
- JDK 25
- Maven 3.9.11
|