From a142238726bdcb3d8b3b4e90b8f7806c5481d943 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:49:44 +0200 Subject: [PATCH] Intern common Train-Case header names Extend NettyRequestFactory's known header-name table with prebuilt Train-Case AsciiString instances for the common headers already covered by lowercase Netty constants. This lets caller-supplied names such as Content-Type and Authorization take the HttpHeadersEncoder bulk-copy path while preserving on-wire casing. Odd casing and custom header names still miss the table and pass through unchanged. Update focused interning tests for Train-Case interning and odd-casing passthrough. Codex on behalf of Pavel Ptashyts Co-Authored-By: Codex --- .../netty/request/NettyRequestFactory.java | 54 +++++++++++++------ ...ettyRequestFactoryHeaderInterningTest.java | 25 ++++++--- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java index b2f23ecbf..0a6e2c802 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -85,18 +85,16 @@ public final class NettyRequestFactory { private static final Integer ZERO_CONTENT_LENGTH = 0; /** - * Canonical (lowercase) header-name spelling -> the pre-built Netty {@link AsciiString} constant. - * Built once. When a user-supplied {@code String} name is byte-identical to a canonical spelling, the - * outbound header uses the shared {@code AsciiString} so {@code HttpHeadersEncoder} hits its bulk - * array-copy branch instead of the per-char US-ASCII encode loop on the event loop. The map is keyed - * case-sensitively, so a name that is not byte-identical to its canonical spelling (e.g. Train-Case - * {@code Content-Type}) misses and is emitted verbatim — on-wire bytes are never altered, and a custom - * name costs a single {@link HashMap#get} miss with zero allocation. + * Known header-name spelling -> a pre-built {@link AsciiString} with the same bytes. Built once. When a + * user-supplied {@code String} name is byte-identical to a known lowercase or Train-Case spelling, the + * outbound header uses a shared {@code AsciiString} so {@code HttpHeadersEncoder} hits its bulk array-copy + * branch instead of the per-char US-ASCII encode loop on the event loop. The map is keyed case-sensitively: + * custom names and odd casing miss and are emitted verbatim, so on-wire bytes are never altered. */ private static final Map KNOWN_HEADER_NAMES = buildKnownHeaderNames(); private static Map buildKnownHeaderNames() { - AsciiString[] names = { + AsciiString[] lowerCaseNames = { HttpHeaderNames.ACCEPT, HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderNames.ACCEPT_LANGUAGE, @@ -112,21 +110,43 @@ private static Map buildKnownHeaderNames() { HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderNames.USER_AGENT }; - Map map = new HashMap<>((int) (names.length / 0.75f) + 1); - for (AsciiString name : names) { - // Key by the constant's own (lowercase) bytes; interning only on a byte-identical match keeps - // the on-wire casing exactly as the caller spelled it. - map.put(name.toString(), name); + AsciiString[] trainCaseNames = { + new AsciiString("Accept"), + new AsciiString("Accept-Encoding"), + new AsciiString("Accept-Language"), + new AsciiString("Authorization"), + new AsciiString("Cache-Control"), + new AsciiString("Connection"), + new AsciiString("Content-Length"), + new AsciiString("Content-Type"), + new AsciiString("Cookie"), + new AsciiString("Host"), + new AsciiString("Origin"), + new AsciiString("Referer"), + new AsciiString("Transfer-Encoding"), + new AsciiString("User-Agent") + }; + Map map = new HashMap<>( + (int) ((lowerCaseNames.length + trainCaseNames.length) / 0.75f) + 1); + for (AsciiString name : lowerCaseNames) { + putKnownHeaderName(map, name); + } + for (AsciiString name : trainCaseNames) { + putKnownHeaderName(map, name); } return map; } + private static void putKnownHeaderName(Map map, AsciiString name) { + map.put(name.toString(), name); + } + /** * Copy {@code source} request headers into the freshly created outbound {@code target}, interning - * recognized header names to their static {@link AsciiString} constant (see {@link #KNOWN_HEADER_NAMES}) - * so the encode hot path bulk-copies instead of per-char encoding. Multi-value headers and ordering are - * preserved (one {@code add} per stored entry, in iteration order). A name already stored as an - * {@code AsciiString} is on the fast path already and passed through untouched. + * recognized header names to their shared {@link AsciiString} (see {@link #KNOWN_HEADER_NAMES}) so the + * encode hot path bulk-copies instead of per-char encoding. Multi-value headers and ordering are preserved + * (one {@code add} per stored entry, in iteration order). A name already stored as an {@code AsciiString} + * is on the fast path already and passed through untouched. */ // package-private for unit testing; not part of the public API. static void copyInternedHeaders(HttpHeaders source, HttpHeaders target) { diff --git a/client/src/test/java/org/asynchttpclient/netty/request/NettyRequestFactoryHeaderInterningTest.java b/client/src/test/java/org/asynchttpclient/netty/request/NettyRequestFactoryHeaderInterningTest.java index 52ff4f950..2aa85c663 100644 --- a/client/src/test/java/org/asynchttpclient/netty/request/NettyRequestFactoryHeaderInterningTest.java +++ b/client/src/test/java/org/asynchttpclient/netty/request/NettyRequestFactoryHeaderInterningTest.java @@ -31,10 +31,9 @@ /** * Tests {@link NettyRequestFactory#copyInternedHeaders(HttpHeaders, HttpHeaders)}: standard header names - * supplied in canonical (lowercase) spelling must be interned to the shared {@link AsciiString} constant - * (so the encoder bulk-copies), while names that are not byte-identical to the canonical spelling, and - * custom names, must be emitted verbatim so on-wire bytes are never altered. Order, values and multi-value - * headers must be preserved. + * supplied in known lowercase or Train-Case spelling must be interned to a shared {@link AsciiString} with + * the same bytes (so the encoder bulk-copies), while odd casing and custom names must be emitted verbatim so + * on-wire bytes are never altered. Order, values and multi-value headers must be preserved. */ public class NettyRequestFactoryHeaderInterningTest { @@ -53,9 +52,8 @@ public void internsCanonicalLowercaseKnownNameToAsciiStringConstant() { } @Test - public void leavesNonCanonicalCasingUntouched() { + public void internsTrainCaseKnownNameToAsciiStringPreservingCasing() { HttpHeaders source = new DefaultHttpHeaders(); - // Train-Case is NOT byte-identical to the lowercase constant, so it must be emitted verbatim. source.add("Content-Type", "application/json"); HttpHeaders target = new DefaultHttpHeaders(); @@ -63,7 +61,20 @@ public void leavesNonCanonicalCasingUntouched() { CharSequence name = firstNameCharSequence(target); assertEquals("Content-Type", name.toString(), "wire casing must be preserved exactly"); - assertTrue(name instanceof String, "non-canonical name must remain the original String, not be interned"); + assertTrue(name instanceof AsciiString, "known Train-Case name must be interned for fast encoding"); + } + + @Test + public void leavesOddCasingUntouched() { + HttpHeaders source = new DefaultHttpHeaders(); + source.add("CONTENT-TYPE", "application/json"); + + HttpHeaders target = new DefaultHttpHeaders(); + NettyRequestFactory.copyInternedHeaders(source, target); + + CharSequence name = firstNameCharSequence(target); + assertEquals("CONTENT-TYPE", name.toString(), "wire casing must be preserved exactly"); + assertTrue(name instanceof String, "unknown casing must remain the original String, not be interned"); } @Test