diff --git a/client/src/jmh/java/org/asynchttpclient/bench/AcceptEncodingHpackBenchmark.java b/client/src/jmh/java/org/asynchttpclient/bench/AcceptEncodingHpackBenchmark.java
index 0e69aae03..31db7deca 100644
--- a/client/src/jmh/java/org/asynchttpclient/bench/AcceptEncodingHpackBenchmark.java
+++ b/client/src/jmh/java/org/asynchttpclient/bench/AcceptEncodingHpackBenchmark.java
@@ -34,15 +34,15 @@
/**
* Measures the HPACK-encoded wire size of {@code accept-encoding} for the two value spellings:
*
- * - AHC current: {@code "gzip,deflate"} (no space) — built in
+ *
- AHC current: {@code "gzip,deflate"} (no space) - built in
* {@code HttpUtils.GZIP_DEFLATE = new AsciiString(GZIP + "," + DEFLATE)}.
* - HPACK static table entry #16: {@code "gzip, deflate"} (with space, RFC 7541 App. A).
*
*
* On a fresh encoder (first request of a connection) the static-table value matches as a single
* indexed byte; the non-matching spelling is literal-encoded and inserted into the dynamic table.
- * This bench reports {@code gc.alloc.rate.norm} and the encoded byte count via the returned buffer's
- * readableBytes (consumed by the blackhole through the return value size).
+ * This bench reports encoding time and, when run with {@code -prof gc}, allocation. The returned encoded
+ * byte count prevents dead-code elimination; {@code AcceptEncodingHpackTest} verifies the wire sizes.
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
index 36af9019b..b1fa1d3fc 100755
--- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
+++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
@@ -106,6 +106,8 @@
import static org.asynchttpclient.util.AuthenticatorUtils.perConnectionProxyAuthorizationHeader;
import static org.asynchttpclient.util.HttpConstants.Methods.CONNECT;
import static org.asynchttpclient.util.HttpConstants.Methods.GET;
+import static org.asynchttpclient.util.HttpUtils.GZIP_DEFLATE;
+import static org.asynchttpclient.util.HttpUtils.GZIP_DEFLATE_HPACK;
import static org.asynchttpclient.util.HttpUtils.hostHeader;
import static org.asynchttpclient.util.MiscUtils.getCause;
import static org.asynchttpclient.util.ProxyUtils.getProxyServer;
@@ -933,7 +935,7 @@ private void sendHttp2Frames(NettyResponseFuture future, Http2StreamChann
&& !HttpHeaderValues.TRAILERS.contentEqualsIgnoreCase(value)) {
continue;
}
- h2Headers.add(toLowerCaseHeaderName(name), value);
+ h2Headers.add(toLowerCaseHeaderName(name), http2HeaderValue(name, value));
}
// Determine the body to send: an in-memory buffer (DefaultFullHttpRequest content or a
@@ -971,6 +973,13 @@ private void sendHttp2Frames(NettyResponseFuture future, Http2StreamChann
}
}
+ private static CharSequence http2HeaderValue(CharSequence name, CharSequence value) {
+ if (value == GZIP_DEFLATE && HttpHeaderNames.ACCEPT_ENCODING.contentEqualsIgnoreCase(name)) {
+ return GZIP_DEFLATE_HPACK;
+ }
+ return value;
+ }
+
/**
* Sends the body of an HTTP/2 request whose HEADERS were already written with {@code endStream=false}
* because it carried {@code Expect: 100-continue}. Invoked by {@code Continue100Interceptor} once the
diff --git a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java
index 3cca41e61..32afd9142 100644
--- a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java
@@ -37,6 +37,8 @@ public final class HttpUtils {
public static final AsciiString ACCEPT_ALL_HEADER_VALUE = new AsciiString("*/*");
public static final AsciiString GZIP_DEFLATE = new AsciiString(HttpHeaderValues.GZIP + "," + HttpHeaderValues.DEFLATE);
+ /** RFC 7541 Appendix A static table entry 16. */
+ public static final AsciiString GZIP_DEFLATE_HPACK = new AsciiString(HttpHeaderValues.GZIP + ", " + HttpHeaderValues.DEFLATE);
private static final String CONTENT_TYPE_CHARSET_ATTRIBUTE = "charset=";
private static final String CONTENT_TYPE_BOUNDARY_ATTRIBUTE = "boundary=";
private static final String BROTLY_ACCEPT_ENCODING_SUFFIX = ", br";
diff --git a/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java b/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
index 9e97a7b42..877b2c3a3 100644
--- a/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
+++ b/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
@@ -26,6 +26,8 @@
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.compression.Brotli;
+import io.netty.handler.codec.compression.Zstd;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.QueryStringDecoder;
@@ -75,6 +77,7 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
+import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -1048,6 +1051,39 @@ public void mixedCaseHeaderIsLowercasedAndConnectionHeadersExcludedOverHttp2() t
}
}
+ @Test
+ public void generatedAcceptEncodingUsesHpackStaticValueOverHttp2() throws Exception {
+ try (AsyncHttpClient client = http2ClientWithConfig(builder -> builder.setCompressionEnforced(true))) {
+ Response response = client.prepareGet(httpsUrl("/echo"))
+ .execute()
+ .get(30, SECONDS);
+
+ assertEquals(200, response.getStatusCode());
+ List expected = new ArrayList<>();
+ expected.add("gzip, deflate");
+ if (Brotli.isAvailable()) {
+ expected.add("br");
+ }
+ if (Zstd.isAvailable()) {
+ expected.add("zstd");
+ }
+ assertEquals(expected, response.getHeaders("X-accept-encoding"));
+ }
+ }
+
+ @Test
+ public void userAcceptEncodingSpellingIsPreservedOverHttp2() throws Exception {
+ try (AsyncHttpClient client = http2ClientWithConfig(builder -> builder.setCompressionEnforced(true))) {
+ Response response = client.prepareGet(httpsUrl("/echo"))
+ .setHeader(ACCEPT_ENCODING, "gzip,deflate")
+ .execute()
+ .get(30, SECONDS);
+
+ assertEquals(200, response.getStatusCode());
+ assertEquals("gzip,deflate", response.getHeader("X-accept-encoding"));
+ }
+ }
+
@Test
public void postWithHeadersAndFormParamsOverHttp2() throws Exception {
try (AsyncHttpClient client = http2Client()) {
diff --git a/client/src/test/java/org/asynchttpclient/BasicHttpTest.java b/client/src/test/java/org/asynchttpclient/BasicHttpTest.java
index 44e7f45df..0e0cff044 100755
--- a/client/src/test/java/org/asynchttpclient/BasicHttpTest.java
+++ b/client/src/test/java/org/asynchttpclient/BasicHttpTest.java
@@ -64,6 +64,7 @@
import static io.netty.handler.codec.http.HttpHeaderNames.TRANSFER_ENCODING;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.config;
import static org.asynchttpclient.Dsl.get;
import static org.asynchttpclient.Dsl.head;
@@ -106,6 +107,18 @@ private String getTargetUrl() {
return server.getHttpUrl() + "/foo/bar";
}
+ @RepeatedIfExceptionsTest(repeats = 5)
+ public void generatedAcceptEncodingKeepsHttp1Spelling() throws Exception {
+ server.enqueueEcho();
+ try (AsyncHttpClient client = asyncHttpClient(config().setCompressionEnforced(true))) {
+ Response response = client.prepareGet(getTargetUrl())
+ .execute()
+ .get(TIMEOUT, SECONDS);
+
+ assertEquals("gzip,deflate", response.getHeader("X-Accept-Encoding"));
+ }
+ }
+
@RepeatedIfExceptionsTest(repeats = 5)
public void getRootUrl() throws Throwable {
withClient().run(client ->
diff --git a/client/src/test/java/org/asynchttpclient/netty/request/AcceptEncodingHpackTest.java b/client/src/test/java/org/asynchttpclient/netty/request/AcceptEncodingHpackTest.java
new file mode 100644
index 000000000..bd0adba56
--- /dev/null
+++ b/client/src/test/java/org/asynchttpclient/netty/request/AcceptEncodingHpackTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.asynchttpclient.netty.request;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.Http2HeadersEncoder;
+import io.netty.util.AsciiString;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Verifies Netty HPACK wire sizes; {@code BasicHttp2Test} covers AHC's production request path. */
+public class AcceptEncodingHpackTest {
+
+ private static final AsciiString ACCEPT_ENCODING = AsciiString.cached("accept-encoding");
+
+ @Test
+ public void staticTableSpellingUsesOneByteIndexedRepresentation() throws Exception {
+ assertTrue(encodedLength("gzip,deflate") > 1);
+ assertEquals(1, encodedLength("gzip, deflate"));
+ }
+
+ private static int encodedLength(String value) throws Exception {
+ Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder();
+ Http2Headers headers = new DefaultHttp2Headers().add(ACCEPT_ENCODING, value);
+ ByteBuf output = Unpooled.buffer();
+ try {
+ encoder.encodeHeaders(3, headers, output);
+ return output.readableBytes();
+ } finally {
+ output.release();
+ }
+ }
+}