Skip to content

Commit b54ed09

Browse files
authored
Add Support for Multiplexing HTTP/2 (#2144)
Motivation: Add HTTP/2 multiplexing support to handle concurrent requests as streams over shared connections, layered on top of the existing connection pool infrastructure. Motivation: - Added `default` HTTP/2 config methods to `AsyncHttpClientConfig` — keeps backward compatibility with custom implementations - Added `Http2Handler`, `Http2PingHandler`, `Http2ContentDecompressor` — handles H2 frames, keepalive, and decompression - Added `Http2ConnectionState` and H2 connection registry in `ChannelManager` — enables stream multiplexing with GOAWAY/MAX_CONCURRENT_STREAMS tracking - Added ALPN in `DefaultSslEngineFactory` and pipeline upgrade in `NettyConnectListener` — negotiates H2 transparently on HTTPS - Added `HttpProtocol` enum and `Response.getProtocol()` — exposes negotiated protocol to callers
1 parent 7657735 commit b54ed09

29 files changed

Lines changed: 2801 additions & 30 deletions

client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,62 @@ public interface AsyncHttpClientConfig {
277277
*/
278278
boolean isFilterInsecureCipherSuites();
279279

280+
/**
281+
* @return true if HTTP/2 is enabled (negotiated via ALPN for HTTPS connections)
282+
*/
283+
default boolean isHttp2Enabled() {
284+
return true;
285+
}
286+
287+
/**
288+
* @return the HTTP/2 initial window size in bytes, defaults to 65535
289+
*/
290+
default int getHttp2InitialWindowSize() {
291+
return 65_535;
292+
}
293+
294+
/**
295+
* @return the HTTP/2 max frame size in bytes, must be between 16384 and 16777215 per RFC 7540 §4.2
296+
*/
297+
default int getHttp2MaxFrameSize() {
298+
return 16_384;
299+
}
300+
301+
/**
302+
* @return the HTTP/2 HPACK header table size in bytes, defaults to 4096
303+
*/
304+
default int getHttp2HeaderTableSize() {
305+
return 4_096;
306+
}
307+
308+
/**
309+
* @return the HTTP/2 max header list size in bytes, defaults to 8192
310+
*/
311+
default int getHttp2MaxHeaderListSize() {
312+
return 8_192;
313+
}
314+
315+
/**
316+
* @return the HTTP/2 max concurrent streams per connection, -1 means unlimited (server-controlled)
317+
*/
318+
default int getHttp2MaxConcurrentStreams() {
319+
return -1;
320+
}
321+
322+
/**
323+
* @return the interval between HTTP/2 PING keepalive frames, {@link Duration#ZERO} disables pinging
324+
*/
325+
default Duration getHttp2PingInterval() {
326+
return Duration.ZERO;
327+
}
328+
329+
/**
330+
* @return true if cleartext HTTP/2 (h2c) via prior knowledge is enabled for non-TLS connections
331+
*/
332+
default boolean isHttp2CleartextEnabled() {
333+
return false;
334+
}
335+
280336
/**
281337
* @return the size of the SSL session cache, 0 means using the default value
282338
*/

client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@
101101
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultUserAgent;
102102
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultValidateResponseHeaders;
103103
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultWebSocketMaxBufferSize;
104+
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultHttp2CleartextEnabled;
105+
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultHttp2HeaderTableSize;
106+
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultHttp2InitialWindowSize;
107+
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultHttp2MaxConcurrentStreams;
108+
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultHttp2MaxFrameSize;
109+
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultHttp2MaxHeaderListSize;
110+
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultHttp2PingInterval;
104111
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultWebSocketMaxFrameSize;
105112

106113
/**
@@ -166,6 +173,14 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
166173
private final int sslSessionTimeout;
167174
private final @Nullable SslContext sslContext;
168175
private final @Nullable SslEngineFactory sslEngineFactory;
176+
private final boolean http2Enabled;
177+
private final int http2InitialWindowSize;
178+
private final int http2MaxFrameSize;
179+
private final int http2HeaderTableSize;
180+
private final int http2MaxHeaderListSize;
181+
private final int http2MaxConcurrentStreams;
182+
private final Duration http2PingInterval;
183+
private final boolean http2CleartextEnabled;
169184

170185
// filters
171186
private final List<RequestFilter> requestFilters;
@@ -253,6 +268,14 @@ private DefaultAsyncHttpClientConfig(// http
253268
int sslSessionTimeout,
254269
@Nullable SslContext sslContext,
255270
@Nullable SslEngineFactory sslEngineFactory,
271+
boolean http2Enabled,
272+
int http2InitialWindowSize,
273+
int http2MaxFrameSize,
274+
int http2HeaderTableSize,
275+
int http2MaxHeaderListSize,
276+
int http2MaxConcurrentStreams,
277+
Duration http2PingInterval,
278+
boolean http2CleartextEnabled,
256279

257280
// filters
258281
List<RequestFilter> requestFilters,
@@ -348,6 +371,14 @@ private DefaultAsyncHttpClientConfig(// http
348371
this.sslSessionTimeout = sslSessionTimeout;
349372
this.sslContext = sslContext;
350373
this.sslEngineFactory = sslEngineFactory;
374+
this.http2Enabled = http2Enabled;
375+
this.http2InitialWindowSize = http2InitialWindowSize;
376+
this.http2MaxFrameSize = http2MaxFrameSize;
377+
this.http2HeaderTableSize = http2HeaderTableSize;
378+
this.http2MaxHeaderListSize = http2MaxHeaderListSize;
379+
this.http2MaxConcurrentStreams = http2MaxConcurrentStreams;
380+
this.http2PingInterval = http2PingInterval;
381+
this.http2CleartextEnabled = http2CleartextEnabled;
351382

352383
// filters
353384
this.requestFilters = requestFilters;
@@ -382,6 +413,14 @@ private DefaultAsyncHttpClientConfig(// http
382413
throw new IllegalArgumentException("Native Transport must be enabled to use Epoll Native Transport only");
383414
}
384415

416+
if (http2MaxFrameSize < 16384 || http2MaxFrameSize > 16777215) {
417+
throw new IllegalArgumentException("HTTP/2 max frame size must be between 16384 and 16777215 per RFC 7540 §4.2");
418+
}
419+
420+
if (http2InitialWindowSize < 0) {
421+
throw new IllegalArgumentException("HTTP/2 initial window size must be non-negative");
422+
}
423+
385424
this.allocator = allocator;
386425
this.nettyTimer = nettyTimer;
387426
this.threadFactory = threadFactory;
@@ -608,6 +647,46 @@ public boolean isFilterInsecureCipherSuites() {
608647
return filterInsecureCipherSuites;
609648
}
610649

650+
@Override
651+
public boolean isHttp2Enabled() {
652+
return http2Enabled;
653+
}
654+
655+
@Override
656+
public int getHttp2InitialWindowSize() {
657+
return http2InitialWindowSize;
658+
}
659+
660+
@Override
661+
public int getHttp2MaxFrameSize() {
662+
return http2MaxFrameSize;
663+
}
664+
665+
@Override
666+
public int getHttp2HeaderTableSize() {
667+
return http2HeaderTableSize;
668+
}
669+
670+
@Override
671+
public int getHttp2MaxHeaderListSize() {
672+
return http2MaxHeaderListSize;
673+
}
674+
675+
@Override
676+
public int getHttp2MaxConcurrentStreams() {
677+
return http2MaxConcurrentStreams;
678+
}
679+
680+
@Override
681+
public Duration getHttp2PingInterval() {
682+
return http2PingInterval;
683+
}
684+
685+
@Override
686+
public boolean isHttp2CleartextEnabled() {
687+
return http2CleartextEnabled;
688+
}
689+
611690
@Override
612691
public int getSslSessionCacheSize() {
613692
return sslSessionCacheSize;
@@ -847,6 +926,14 @@ public static class Builder {
847926
private int sslSessionTimeout = defaultSslSessionTimeout();
848927
private @Nullable SslContext sslContext;
849928
private @Nullable SslEngineFactory sslEngineFactory;
929+
private boolean http2Enabled = true;
930+
private int http2InitialWindowSize = defaultHttp2InitialWindowSize();
931+
private int http2MaxFrameSize = defaultHttp2MaxFrameSize();
932+
private int http2HeaderTableSize = defaultHttp2HeaderTableSize();
933+
private int http2MaxHeaderListSize = defaultHttp2MaxHeaderListSize();
934+
private int http2MaxConcurrentStreams = defaultHttp2MaxConcurrentStreams();
935+
private Duration http2PingInterval = defaultHttp2PingInterval();
936+
private boolean http2CleartextEnabled = defaultHttp2CleartextEnabled();
850937

851938
// cookie store
852939
private CookieStore cookieStore = new ThreadSafeCookieStore();
@@ -939,6 +1026,14 @@ public Builder(AsyncHttpClientConfig config) {
9391026
sslSessionTimeout = config.getSslSessionTimeout();
9401027
sslContext = config.getSslContext();
9411028
sslEngineFactory = config.getSslEngineFactory();
1029+
http2Enabled = config.isHttp2Enabled();
1030+
http2InitialWindowSize = config.getHttp2InitialWindowSize();
1031+
http2MaxFrameSize = config.getHttp2MaxFrameSize();
1032+
http2HeaderTableSize = config.getHttp2HeaderTableSize();
1033+
http2MaxHeaderListSize = config.getHttp2MaxHeaderListSize();
1034+
http2MaxConcurrentStreams = config.getHttp2MaxConcurrentStreams();
1035+
http2PingInterval = config.getHttp2PingInterval();
1036+
http2CleartextEnabled = config.isHttp2CleartextEnabled();
9421037

9431038
// filters
9441039
requestFilters.addAll(config.getRequestFilters());
@@ -1254,6 +1349,46 @@ public Builder setSslEngineFactory(SslEngineFactory sslEngineFactory) {
12541349
return this;
12551350
}
12561351

1352+
public Builder setHttp2Enabled(boolean http2Enabled) {
1353+
this.http2Enabled = http2Enabled;
1354+
return this;
1355+
}
1356+
1357+
public Builder setHttp2InitialWindowSize(int http2InitialWindowSize) {
1358+
this.http2InitialWindowSize = http2InitialWindowSize;
1359+
return this;
1360+
}
1361+
1362+
public Builder setHttp2MaxFrameSize(int http2MaxFrameSize) {
1363+
this.http2MaxFrameSize = http2MaxFrameSize;
1364+
return this;
1365+
}
1366+
1367+
public Builder setHttp2HeaderTableSize(int http2HeaderTableSize) {
1368+
this.http2HeaderTableSize = http2HeaderTableSize;
1369+
return this;
1370+
}
1371+
1372+
public Builder setHttp2MaxHeaderListSize(int http2MaxHeaderListSize) {
1373+
this.http2MaxHeaderListSize = http2MaxHeaderListSize;
1374+
return this;
1375+
}
1376+
1377+
public Builder setHttp2MaxConcurrentStreams(int http2MaxConcurrentStreams) {
1378+
this.http2MaxConcurrentStreams = http2MaxConcurrentStreams;
1379+
return this;
1380+
}
1381+
1382+
public Builder setHttp2PingInterval(Duration http2PingInterval) {
1383+
this.http2PingInterval = http2PingInterval;
1384+
return this;
1385+
}
1386+
1387+
public Builder setHttp2CleartextEnabled(boolean http2CleartextEnabled) {
1388+
this.http2CleartextEnabled = http2CleartextEnabled;
1389+
return this;
1390+
}
1391+
12571392
// filters
12581393
public Builder addRequestFilter(RequestFilter requestFilter) {
12591394
requestFilters.add(requestFilter);
@@ -1486,6 +1621,14 @@ public DefaultAsyncHttpClientConfig build() {
14861621
sslSessionTimeout,
14871622
sslContext,
14881623
sslEngineFactory,
1624+
http2Enabled,
1625+
http2InitialWindowSize,
1626+
http2MaxFrameSize,
1627+
http2HeaderTableSize,
1628+
http2MaxHeaderListSize,
1629+
http2MaxConcurrentStreams,
1630+
http2PingInterval,
1631+
http2CleartextEnabled,
14891632
requestFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(requestFilters),
14901633
responseFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(responseFilters),
14911634
ioExceptionFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(ioExceptionFilters),
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2014-2026 AsyncHttpClient Project. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.asynchttpclient;
17+
18+
/**
19+
* HTTP protocol version used for a request/response exchange.
20+
*/
21+
public enum HttpProtocol {
22+
23+
HTTP_1_0("HTTP/1.0"),
24+
HTTP_1_1("HTTP/1.1"),
25+
HTTP_2("HTTP/2");
26+
27+
private final String text;
28+
29+
HttpProtocol(String text) {
30+
this.text = text;
31+
}
32+
33+
/**
34+
* @return the protocol version string (e.g. "HTTP/1.1", "HTTP/2")
35+
*/
36+
public String getText() {
37+
return text;
38+
}
39+
40+
@Override
41+
public String toString() {
42+
return text;
43+
}
44+
}

client/src/main/java/org/asynchttpclient/Response.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ public interface Response {
169169
*/
170170
boolean hasResponseBody();
171171

172+
/**
173+
* Return the HTTP protocol version used for this response.
174+
*
175+
* @return the protocol, defaults to {@link HttpProtocol#HTTP_1_1}
176+
*/
177+
default HttpProtocol getProtocol() {
178+
return HttpProtocol.HTTP_1_1;
179+
}
180+
172181
/**
173182
* Get the remote address that the client initiated the request to.
174183
*

client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ public final class AsyncHttpClientConfigDefaults {
8383
public static final String HASHED_WHEEL_TIMER_TICK_DURATION = "hashedWheelTimerTickDuration";
8484
public static final String HASHED_WHEEL_TIMER_SIZE = "hashedWheelTimerSize";
8585
public static final String EXPIRED_COOKIE_EVICTION_DELAY = "expiredCookieEvictionDelay";
86+
public static final String HTTP2_INITIAL_WINDOW_SIZE_CONFIG = "http2InitialWindowSize";
87+
public static final String HTTP2_MAX_FRAME_SIZE_CONFIG = "http2MaxFrameSize";
88+
public static final String HTTP2_HEADER_TABLE_SIZE_CONFIG = "http2HeaderTableSize";
89+
public static final String HTTP2_MAX_HEADER_LIST_SIZE_CONFIG = "http2MaxHeaderListSize";
90+
public static final String HTTP2_MAX_CONCURRENT_STREAMS_CONFIG = "http2MaxConcurrentStreams";
91+
public static final String HTTP2_PING_INTERVAL_CONFIG = "http2PingInterval";
92+
public static final String HTTP2_CLEARTEXT_ENABLED_CONFIG = "http2CleartextEnabled";
8693

8794
public static final String AHC_VERSION;
8895

@@ -332,4 +339,32 @@ public static int defaultHashedWheelTimerSize() {
332339
public static int defaultExpiredCookieEvictionDelay() {
333340
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + EXPIRED_COOKIE_EVICTION_DELAY);
334341
}
342+
343+
public static int defaultHttp2InitialWindowSize() {
344+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HTTP2_INITIAL_WINDOW_SIZE_CONFIG);
345+
}
346+
347+
public static int defaultHttp2MaxFrameSize() {
348+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HTTP2_MAX_FRAME_SIZE_CONFIG);
349+
}
350+
351+
public static int defaultHttp2HeaderTableSize() {
352+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HTTP2_HEADER_TABLE_SIZE_CONFIG);
353+
}
354+
355+
public static int defaultHttp2MaxHeaderListSize() {
356+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HTTP2_MAX_HEADER_LIST_SIZE_CONFIG);
357+
}
358+
359+
public static int defaultHttp2MaxConcurrentStreams() {
360+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HTTP2_MAX_CONCURRENT_STREAMS_CONFIG);
361+
}
362+
363+
public static Duration defaultHttp2PingInterval() {
364+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getDuration(ASYNC_CLIENT_CONFIG_ROOT + HTTP2_PING_INTERVAL_CONFIG);
365+
}
366+
367+
public static boolean defaultHttp2CleartextEnabled() {
368+
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + HTTP2_CLEARTEXT_ENABLED_CONFIG);
369+
}
335370
}

0 commit comments

Comments
 (0)