Why Spring MVC Auto‑Binds Header Data to Your DTO – The Hidden Mechanism Explained
This article explores why Spring MVC automatically binds HTTP header values to controller method parameters, demonstrates the issue with a sample Spring Boot 3.4.0 project, analyzes the underlying HandlerMethodArgumentResolver and ServletModelAttributeMethodProcessor code, and provides two practical solutions to prevent unwanted header binding.
Spring Boot 3.4.0 project includes an endpoint @RestController with @RequestMapping("/client") and a method public ResponseEntity<ClientParam> find(ClientParam clientParam) that returns the received object.
The ClientParam class contains fields clientId , clientKey , accept , and host with getters and setters.
When the endpoint is accessed via a browser with a query string such as /client?clientId=pack , the response shows that accept and host are also populated, even though they were not supplied.
Root cause analysis
Spring MVC resolves method arguments using HandlerMethodArgumentResolver . For a plain object without @RequestBody , the resolver is ServletModelAttributeMethodProcessor , which delegates to ModelAttributeMethodProcessor .
<code>public final Object resolveArgument(...) {
String name = ModelFactory.getNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, type);
constructAttribute(binder, webRequest);
bindRequestParameters(binder, webRequest);
}
</code>The bindRequestParameters method eventually calls ServletRequestDataBinder.bind , which creates a MutablePropertyValues from both request parameters and request headers:
<code>MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
addBindValues(mpvs, request);
doBind(mpvs);
</code>The addBindValues implementation iterates over all HTTP header names, converts them to property names, and adds them to the property values:
<code>if (request instanceof HttpServletRequest httpRequest) {
Enumeration<String> names = httpRequest.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
Object value = getHeaderValue(httpRequest, name);
if (value != null) {
name = StringUtils.uncapitalize(name.replace("-", ""));
addValueIfNotPresent(mpvs, "Header", name, value);
}
}
}
</code>Therefore, header fields such as Accept and Host are bound to the corresponding properties of ClientParam , which explains the observed behavior.
Solutions
Solution 1 (recommended): Create a custom ExtendedServletRequestDataBinder that overrides addBindValues and removes the header‑binding logic, then register it in place of the default binder.
<code>public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
@Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
Map<String, String> uriVars = getUriVars(request);
if (uriVars != null) {
uriVars.forEach((name, value) ->
addValueIfNotPresent(mpvs, "URI variable", name, value));
}
super.addBindValues(mpvs, request); // header binding omitted
}
}
</code>Solution 2: Override RequestMappingHandlerAdapter to customize the argument resolution process. This approach is more invasive and shown here for learning purposes only.
Note that the automatic header binding was introduced in Spring 6.2.0 (used by Spring Boot 3.4.0). Versions prior to this do not exhibit the issue.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.