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..77fdf214f 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,7 @@
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.hostHeader;
import static org.asynchttpclient.util.MiscUtils.getCause;
import static org.asynchttpclient.util.ProxyUtils.getProxyServer;
@@ -113,6 +114,8 @@
public final class NettyRequestSender {
private static final Logger LOGGER = LoggerFactory.getLogger(NettyRequestSender.class);
+ private static final AsciiString HTTP2_HPACK_GZIP_DEFLATE =
+ new AsciiString(HttpHeaderValues.GZIP + ", " + HttpHeaderValues.DEFLATE);
private final AsyncHttpClientConfig config;
private final ChannelManager channelManager;
@@ -918,6 +921,7 @@ private void sendHttp2Frames(NettyResponseFuture future, Http2StreamChann
// Copy the HTTP/1.1 headers, dropping connection-specific names forbidden in HTTP/2 (RFC 7540
// §8.1.2.2). iteratorCharSequence() avoids the per-name String the String-typed iterator forces;
// see isHttp2ExcludedHeader and toLowerCaseHeaderName for the skip-check and lowercasing rules.
+ boolean preferHpackAcceptEncoding = preferHpackAcceptEncoding(future.getCurrentRequest());
Iterator> it = httpRequest.headers().iteratorCharSequence();
while (it.hasNext()) {
Map.Entry entry = it.next();
@@ -933,7 +937,7 @@ private void sendHttp2Frames(NettyResponseFuture future, Http2StreamChann
&& !HttpHeaderValues.TRAILERS.contentEqualsIgnoreCase(value)) {
continue;
}
- h2Headers.add(toLowerCaseHeaderName(name), value);
+ h2Headers.add(toLowerCaseHeaderName(name), http2HeaderValue(name, value, preferHpackAcceptEncoding));
}
// Determine the body to send: an in-memory buffer (DefaultFullHttpRequest content or a
@@ -971,6 +975,20 @@ private void sendHttp2Frames(NettyResponseFuture future, Http2StreamChann
}
}
+ private boolean preferHpackAcceptEncoding(Request request) {
+ return config.isCompressionEnforced()
+ && !request.getHeaders().contains(HttpHeaderNames.ACCEPT_ENCODING);
+ }
+
+ private static CharSequence http2HeaderValue(CharSequence name, CharSequence value, boolean preferHpackAcceptEncoding) {
+ if (preferHpackAcceptEncoding
+ && HttpHeaderNames.ACCEPT_ENCODING.contentEqualsIgnoreCase(name)
+ && GZIP_DEFLATE.contentEquals(value)) {
+ return HTTP2_HPACK_GZIP_DEFLATE;
+ }
+ 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/test/java/org/asynchttpclient/BasicHttp2Test.java b/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
index 9e97a7b42..ff6b0c34f 100644
--- a/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
+++ b/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
@@ -75,6 +75,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 +1049,31 @@ 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());
+ assertEquals("gzip, deflate", response.getHeader("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/netty/request/AcceptEncodingHpackTest.java b/client/src/test/java/org/asynchttpclient/netty/request/AcceptEncodingHpackTest.java
new file mode 100644
index 000000000..b09fa0335
--- /dev/null
+++ b/client/src/test/java/org/asynchttpclient/netty/request/AcceptEncodingHpackTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+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();
+ }
+ }
+}