The story of how HTTP/1.1 request headers are no longer converted to lowercase in Tomcat 11

The story of how HTTP/1.1 request headers are no longer converted to lowercase in Tomcat 11

2026.04.14

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.

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.

References

Share this article