
The story of how HTTP/1.1 request headers are no longer converted to lowercase in Tomcat 11
This page has been translated by machine translation. View original
Hello. I'm Miyabe from the Retail Distribution Solutions Department.
While working on a major version upgrade of Spring Boot, I noticed that the lowercase conversion of HTTP/1.1 request header names is no longer performed from a specific version of Tomcat 11.
Looking at the changelog, the same change was backported to Tomcat 9/10 as well, but there's a slightly complicated situation where only Tomcat 11 is affected, so I'll organize the information.
How I noticed this
After switching to Tomcat 11 as part of a Spring Boot major version upgrade, two tests failed.
Access log format no longer matches
The assertion was based on the assumption that request header names in logs would be lowercase, but they no longer matched the actual values.
Could not mask Cookie header values
We were determining masking targets by comparing header names with "cookie", but the test results showed they weren't being masked.
What changed
In Tomcat 11.0.12 (released 2025-10-07), the following change was introduced:
Store HTTP request headers using the original case for the header name rather than forcing it to lower case.
This is documented in the Coyote section of the Apache Tomcat 11 Changelog.
Previously, Tomcat was converting HTTP/1.1 request header names to lowercase.
For example, if a client sent a header as Content-Type, it was stored as content-type.
With this change, header names are now stored in their original case as sent by the client.
Actual code changes
The change itself was just removing a few lines from HttpHeaderParser.java.
// Removed code
private static final byte A = (byte) 'A';
private static final byte a = (byte) 'a';
private static final byte Z = (byte) 'Z';
private static final byte LC_OFFSET = A - a;
// ...
// chr is next byte of header name. Convert to lowercase.
if (chr >= A && chr <= Z) {
source.getHeaderByteBuffer().put(pos, (byte) (chr - LC_OFFSET));
}
The code that detected uppercase A-Z and converted them to lowercase has been removed.
Commit (11.0.x): e70eb689 - Store request header names using original case rather than forcing to lc
The original purpose was trailer headers
The original purpose of this change was to stop forcing lowercase for HTTP trailer headers.
Trailer headers are headers sent after the body in chunked transfer encoding.
The changes were committed simultaneously to three versions: Tomcat 9/10/11.
| Branch | Commit | First release version |
|---|---|---|
| 9.0.x | aa23770 |
9.0.110 |
| 10.1.x | bbb54e8 |
10.1.47 |
| 11.0.x | e70eb68 |
11.0.12 |
Why only Tomcat 11 was affected for all request headers
The same changes to HttpHeaderParser.java were applied to all three versions, but the scope of impact differs.
This is because the callers of HttpHeaderParser are different depending on the version.
Callers in Tomcat 9/10
| Caller | Purpose |
|---|---|
ChunkedInputFilter |
Parsing trailer headers |
Callers in Tomcat 11
| Caller | Purpose |
|---|---|
ChunkedInputFilter |
Parsing trailer headers |
Http11InputBuffer |
Parsing request headers |
In Tomcat 11, the HTTP/1.1 request header parsing process was refactored to use HttpHeaderParser.
e5acf2cf - Refactor HTTP header parsing to use common parser, 11.0.0-M20 and later
As a result, the change intended for trailer headers has spread to all request headers.
In Tomcat 9/10, Http11InputBuffer doesn't use HttpHeaderParser and performs lowercase conversion internally, so normal request headers aren't affected.
Only trailer header lowercase conversion was discontinued, as intended.
Changelog correction for Tomcat 9/10
Due to this difference in impact scope, the changelogs for Tomcat 9/10 have been corrected.
As of April 14, 2026, these corrections are not yet reflected on the changelog pages, but the commits have been made.
Original changelog:
Store HTTP request headers using the original case for the header name rather than forcing it to lower case.
Corrected changelog:
Store HTTP trailer headers using the original case for the header name rather than forcing it to lower case.
"request headers" has been rewritten as "trailer headers".
Since only trailer headers are actually affected in Tomcat 9/10, the changelog description has been adjusted to match reality.
| Branch | Changelog correction commit |
|---|---|
| 9.0.x | 1441f0a |
| 10.1.x | 10a1cef |
Of course, the Tomcat 11 changelog hasn't been corrected.
This is because in Tomcat 11, all request headers are actually affected, so the original description is correct.
Is this not a breaking change?
Looking at the changelog, it's labeled the same as other bug fixes, but it seems like a significant change.
RFC 7230 Section 3.2 states:
Each header field consists of a case-insensitive field name
Since HTTP header names are defined as case-insensitive, stopping the lowercase conversion isn't a violation of the specification. HttpServletRequest#getHeader() also searches case-insensitively, so normal application code shouldn't be affected.
In other words, this isn't a breaking change.
Nevertheless, caution is needed in actual operations
While there's no issue from an RFC perspective, there may be some impacts.
Access log searching
Previous access logs might have recorded all header names in lowercase.
In Tomcat 11.0.12 and later, they will be recorded in their original case like Content-Type, so you might need to review your log search queries.
# Before: all lowercase
content-type: application/json
# After 11.0.12: original case as sent by the client
# Both uppercase and lowercase are possible
Content-Type: application/json
content-type: application/json
Code that directly compares header names
getHeader() is case-insensitive, but if you have code that directly compares header names as strings, it might be affected.
// This is fine (case-insensitive)
request.getHeader("cookie");
// This may be affected
Collections.list(request.getHeaderNames()).forEach(name -> {
if (name.equals("cookie")) {
log.info("{}: [MASKED]", name);
} else {
log.info("{}: {}", name, request.getHeader(name));
}
});
Summary
Despite the same code change being backported to three versions, the scope of impact differs due to differences in internal architecture refactoring status, making this a change that's quite difficult to notice.
Please be aware that in some cases, processing might be affected if you have code that directly compares header names assuming they are lowercase, or if you use log search queries based on that assumption.