Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@
/**
* Measures the HPACK-encoded wire size of {@code accept-encoding} for the two value spellings:
* <ul>
* <li>AHC current: {@code "gzip,deflate"} (no space) built in
* <li>AHC current: {@code "gzip,deflate"} (no space) - built in
* {@code HttpUtils.GZIP_DEFLATE = new AsciiString(GZIP + "," + DEFLATE)}.</li>
* <li>HPACK static table entry #16: {@code "gzip, deflate"} (with space, RFC 7541 App. A).</li>
* </ul>
*
* <p>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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -933,7 +935,7 @@ private <T> void sendHttp2Frames(NettyResponseFuture<T> 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
Expand Down Expand Up @@ -971,6 +973,13 @@ private <T> void sendHttp2Frames(NettyResponseFuture<T> 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
Expand Down
2 changes: 2 additions & 0 deletions client/src/main/java/org/asynchttpclient/util/HttpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
36 changes: 36 additions & 0 deletions client/src/test/java/org/asynchttpclient/BasicHttp2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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()) {
Expand Down
13 changes: 13 additions & 0 deletions client/src/test/java/org/asynchttpclient/BasicHttpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading