Skip to content

Commit 54a5a8f

Browse files
committed
Add HTTP/2 support
1 parent 0ae2375 commit 54a5a8f

15 files changed

+1646
-9
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ 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+
boolean isHttp2Enabled();
284+
280285
/**
281286
* @return the size of the SSL session cache, 0 means using the default value
282287
*/

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
166166
private final int sslSessionTimeout;
167167
private final @Nullable SslContext sslContext;
168168
private final @Nullable SslEngineFactory sslEngineFactory;
169+
private final boolean http2Enabled;
169170

170171
// filters
171172
private final List<RequestFilter> requestFilters;
@@ -253,6 +254,7 @@ private DefaultAsyncHttpClientConfig(// http
253254
int sslSessionTimeout,
254255
@Nullable SslContext sslContext,
255256
@Nullable SslEngineFactory sslEngineFactory,
257+
boolean http2Enabled,
256258

257259
// filters
258260
List<RequestFilter> requestFilters,
@@ -348,6 +350,7 @@ private DefaultAsyncHttpClientConfig(// http
348350
this.sslSessionTimeout = sslSessionTimeout;
349351
this.sslContext = sslContext;
350352
this.sslEngineFactory = sslEngineFactory;
353+
this.http2Enabled = http2Enabled;
351354

352355
// filters
353356
this.requestFilters = requestFilters;
@@ -608,6 +611,11 @@ public boolean isFilterInsecureCipherSuites() {
608611
return filterInsecureCipherSuites;
609612
}
610613

614+
@Override
615+
public boolean isHttp2Enabled() {
616+
return http2Enabled;
617+
}
618+
611619
@Override
612620
public int getSslSessionCacheSize() {
613621
return sslSessionCacheSize;
@@ -847,6 +855,7 @@ public static class Builder {
847855
private int sslSessionTimeout = defaultSslSessionTimeout();
848856
private @Nullable SslContext sslContext;
849857
private @Nullable SslEngineFactory sslEngineFactory;
858+
private boolean http2Enabled = true;
850859

851860
// cookie store
852861
private CookieStore cookieStore = new ThreadSafeCookieStore();
@@ -939,6 +948,7 @@ public Builder(AsyncHttpClientConfig config) {
939948
sslSessionTimeout = config.getSslSessionTimeout();
940949
sslContext = config.getSslContext();
941950
sslEngineFactory = config.getSslEngineFactory();
951+
http2Enabled = config.isHttp2Enabled();
942952

943953
// filters
944954
requestFilters.addAll(config.getRequestFilters());
@@ -1254,6 +1264,11 @@ public Builder setSslEngineFactory(SslEngineFactory sslEngineFactory) {
12541264
return this;
12551265
}
12561266

1267+
public Builder setHttp2Enabled(boolean http2Enabled) {
1268+
this.http2Enabled = http2Enabled;
1269+
return this;
1270+
}
1271+
12571272
// filters
12581273
public Builder addRequestFilter(RequestFilter requestFilter) {
12591274
requestFilters.add(requestFilter);
@@ -1486,6 +1501,7 @@ public DefaultAsyncHttpClientConfig build() {
14861501
sslSessionTimeout,
14871502
sslContext,
14881503
sslEngineFactory,
1504+
http2Enabled,
14891505
requestFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(requestFilters),
14901506
responseFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(responseFilters),
14911507
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-2024 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.0");
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.0")
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/netty/NettyResponse.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.netty.handler.codec.http.HttpHeaders;
2323
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
2424
import io.netty.handler.codec.http.cookie.Cookie;
25+
import org.asynchttpclient.HttpProtocol;
2526
import org.asynchttpclient.HttpResponseBodyPart;
2627
import org.asynchttpclient.HttpResponseStatus;
2728
import org.asynchttpclient.Response;
@@ -158,6 +159,20 @@ public List<Cookie> getCookies() {
158159

159160
}
160161

162+
@Override
163+
public HttpProtocol getProtocol() {
164+
if (status == null) {
165+
return HttpProtocol.HTTP_1_1;
166+
}
167+
int major = status.getProtocolMajorVersion();
168+
if (major == 2) {
169+
return HttpProtocol.HTTP_2;
170+
} else if (status.getProtocolMinorVersion() == 0) {
171+
return HttpProtocol.HTTP_1_0;
172+
}
173+
return HttpProtocol.HTTP_1_1;
174+
}
175+
161176
@Override
162177
public boolean hasResponseStatus() {
163178
return status != null;
@@ -223,6 +238,7 @@ public InputStream getResponseBodyAsStream() {
223238
public String toString() {
224239
StringBuilder sb = new StringBuilder();
225240
sb.append(getClass().getSimpleName()).append(" {\n")
241+
.append("\tprotocol=").append(getProtocol()).append('\n')
226242
.append("\tstatusCode=").append(getStatusCode()).append('\n')
227243
.append("\theaders=\n");
228244

client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder;
3535
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
3636
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
37+
import io.netty.handler.codec.http2.Http2FrameCodec;
38+
import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
39+
import io.netty.handler.codec.http2.Http2MultiplexHandler;
40+
import io.netty.handler.codec.http2.Http2Settings;
3741
import io.netty.handler.logging.LogLevel;
3842
import io.netty.handler.logging.LoggingHandler;
3943
import io.netty.handler.proxy.ProxyHandler;
@@ -61,6 +65,7 @@
6165
import org.asynchttpclient.netty.NettyResponseFuture;
6266
import org.asynchttpclient.netty.OnLastHttpContentCallback;
6367
import org.asynchttpclient.netty.handler.AsyncHttpClientHandler;
68+
import org.asynchttpclient.netty.handler.Http2Handler;
6469
import org.asynchttpclient.netty.handler.HttpHandler;
6570
import org.asynchttpclient.netty.handler.WebSocketHandler;
6671
import org.asynchttpclient.netty.request.NettyRequestSender;
@@ -96,6 +101,9 @@ public class ChannelManager {
96101
public static final String AHC_HTTP_HANDLER = "ahc-http";
97102
public static final String AHC_WS_HANDLER = "ahc-ws";
98103
public static final String LOGGING_HANDLER = "logging";
104+
public static final String HTTP2_FRAME_CODEC = "http2-frame-codec";
105+
public static final String HTTP2_MULTIPLEX = "http2-multiplex";
106+
public static final String AHC_HTTP2_HANDLER = "ahc-http2";
99107
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelManager.class);
100108
private final AsyncHttpClientConfig config;
101109
private final SslEngineFactory sslEngineFactory;
@@ -109,6 +117,7 @@ public class ChannelManager {
109117
private final ChannelGroup openChannels;
110118

111119
private AsyncHttpClientHandler wsHandler;
120+
private Http2Handler http2Handler;
112121

113122
private boolean isInstanceof(Object object, String name) {
114123
final Class<?> clazz;
@@ -239,6 +248,7 @@ private static Bootstrap newBootstrap(ChannelFactory<? extends Channel> channelF
239248
public void configureBootstraps(NettyRequestSender requestSender) {
240249
final AsyncHttpClientHandler httpHandler = new HttpHandler(config, this, requestSender);
241250
wsHandler = new WebSocketHandler(config, this, requestSender);
251+
http2Handler = new Http2Handler(config, this, requestSender);
242252

243253
httpBootstrap.handler(new ChannelInitializer<Channel>() {
244254
@Override
@@ -549,6 +559,58 @@ protected void initChannel(Channel channel) throws Exception {
549559
return promise;
550560
}
551561

562+
/**
563+
* Checks whether the given channel is an HTTP/2 connection (i.e. has the HTTP/2 multiplex handler installed).
564+
*/
565+
public static boolean isHttp2(Channel channel) {
566+
return channel.pipeline().get(HTTP2_MULTIPLEX) != null;
567+
}
568+
569+
/**
570+
* Returns the shared {@link Http2Handler} instance for use with stream child channels.
571+
*/
572+
public Http2Handler getHttp2Handler() {
573+
return http2Handler;
574+
}
575+
576+
/**
577+
* Upgrades the pipeline from HTTP/1.1 to HTTP/2 after ALPN negotiates "h2".
578+
* Removes HTTP/1.1 handlers and adds {@link Http2FrameCodec} + {@link Http2MultiplexHandler}.
579+
* The per-stream {@link Http2Handler} is added separately on each stream child channel.
580+
*/
581+
public void upgradePipelineToHttp2(ChannelPipeline pipeline) {
582+
// Remove HTTP/1.1 specific handlers
583+
if (pipeline.get(HTTP_CLIENT_CODEC) != null) {
584+
pipeline.remove(HTTP_CLIENT_CODEC);
585+
}
586+
if (pipeline.get(INFLATER_HANDLER) != null) {
587+
pipeline.remove(INFLATER_HANDLER);
588+
}
589+
if (pipeline.get(CHUNKED_WRITER_HANDLER) != null) {
590+
pipeline.remove(CHUNKED_WRITER_HANDLER);
591+
}
592+
if (pipeline.get(AHC_HTTP_HANDLER) != null) {
593+
pipeline.remove(AHC_HTTP_HANDLER);
594+
}
595+
596+
// Add HTTP/2 frame codec (handles connection preface, SETTINGS, PING, flow control, etc.)
597+
Http2FrameCodec frameCodec = Http2FrameCodecBuilder.forClient()
598+
.initialSettings(Http2Settings.defaultSettings())
599+
.build();
600+
601+
// Http2MultiplexHandler creates a child channel per HTTP/2 stream.
602+
// Server-push streams are silently ignored (no-op initializer) since AHC is client-only.
603+
Http2MultiplexHandler multiplexHandler = new Http2MultiplexHandler(new ChannelInitializer<Channel>() {
604+
@Override
605+
protected void initChannel(Channel ch) {
606+
// Server push not supported — ignore inbound pushed streams
607+
}
608+
});
609+
610+
pipeline.addLast(HTTP2_FRAME_CODEC, frameCodec);
611+
pipeline.addLast(HTTP2_MULTIPLEX, multiplexHandler);
612+
}
613+
552614
public void upgradePipelineForWebSockets(ChannelPipeline pipeline) {
553615
pipeline.addAfter(HTTP_CLIENT_CODEC, WS_ENCODER_HANDLER, new WebSocket08FrameEncoder(true));
554616
pipeline.addAfter(WS_ENCODER_HANDLER, WS_DECODER_HANDLER, new WebSocket08FrameDecoder(false,

client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import io.netty.channel.Channel;
1919
import io.netty.handler.codec.http.HttpRequest;
20+
import io.netty.handler.ssl.ApplicationProtocolNames;
2021
import io.netty.handler.ssl.SslHandler;
2122
import org.asynchttpclient.AsyncHandler;
2223
import org.asynchttpclient.Request;
@@ -185,6 +186,11 @@ protected void onSuccess(Channel value) {
185186
NettyConnectListener.this.onFailure(channel, e);
186187
return;
187188
}
189+
// Detect ALPN-negotiated protocol and upgrade pipeline to HTTP/2 if "h2" was selected
190+
String alpnProtocol = sslHandler.applicationProtocol();
191+
if (ApplicationProtocolNames.HTTP_2.equals(alpnProtocol)) {
192+
channelManager.upgradePipelineToHttp2(channel.pipeline());
193+
}
188194
writeRequest(channel);
189195
}
190196

0 commit comments

Comments
 (0)