Skip to content
Open
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 @@ -277,6 +277,11 @@ public interface AsyncHttpClientConfig {
*/
boolean isFilterInsecureCipherSuites();

/**
* @return true if HTTP/2 is enabled (negotiated via ALPN for HTTPS connections)
*/
boolean isHttp2Enabled();

/**
* @return the size of the SSL session cache, 0 means using the default value
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
private final int sslSessionTimeout;
private final @Nullable SslContext sslContext;
private final @Nullable SslEngineFactory sslEngineFactory;
private final boolean http2Enabled;

// filters
private final List<RequestFilter> requestFilters;
Expand Down Expand Up @@ -253,6 +254,7 @@ private DefaultAsyncHttpClientConfig(// http
int sslSessionTimeout,
@Nullable SslContext sslContext,
@Nullable SslEngineFactory sslEngineFactory,
boolean http2Enabled,

// filters
List<RequestFilter> requestFilters,
Expand Down Expand Up @@ -348,6 +350,7 @@ private DefaultAsyncHttpClientConfig(// http
this.sslSessionTimeout = sslSessionTimeout;
this.sslContext = sslContext;
this.sslEngineFactory = sslEngineFactory;
this.http2Enabled = http2Enabled;

// filters
this.requestFilters = requestFilters;
Expand Down Expand Up @@ -608,6 +611,11 @@ public boolean isFilterInsecureCipherSuites() {
return filterInsecureCipherSuites;
}

@Override
public boolean isHttp2Enabled() {
return http2Enabled;
}

@Override
public int getSslSessionCacheSize() {
return sslSessionCacheSize;
Expand Down Expand Up @@ -847,6 +855,7 @@ public static class Builder {
private int sslSessionTimeout = defaultSslSessionTimeout();
private @Nullable SslContext sslContext;
private @Nullable SslEngineFactory sslEngineFactory;
private boolean http2Enabled = true;

// cookie store
private CookieStore cookieStore = new ThreadSafeCookieStore();
Expand Down Expand Up @@ -939,6 +948,7 @@ public Builder(AsyncHttpClientConfig config) {
sslSessionTimeout = config.getSslSessionTimeout();
sslContext = config.getSslContext();
sslEngineFactory = config.getSslEngineFactory();
http2Enabled = config.isHttp2Enabled();

// filters
requestFilters.addAll(config.getRequestFilters());
Expand Down Expand Up @@ -1254,6 +1264,11 @@ public Builder setSslEngineFactory(SslEngineFactory sslEngineFactory) {
return this;
}

public Builder setHttp2Enabled(boolean http2Enabled) {
this.http2Enabled = http2Enabled;
return this;
}

// filters
public Builder addRequestFilter(RequestFilter requestFilter) {
requestFilters.add(requestFilter);
Expand Down Expand Up @@ -1486,6 +1501,7 @@ public DefaultAsyncHttpClientConfig build() {
sslSessionTimeout,
sslContext,
sslEngineFactory,
http2Enabled,
requestFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(requestFilters),
responseFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(responseFilters),
ioExceptionFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(ioExceptionFilters),
Expand Down
44 changes: 44 additions & 0 deletions client/src/main/java/org/asynchttpclient/HttpProtocol.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2014-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;

/**
* HTTP protocol version used for a request/response exchange.
*/
public enum HttpProtocol {

HTTP_1_0("HTTP/1.0"),
HTTP_1_1("HTTP/1.1"),
HTTP_2("HTTP/2.0");

private final String text;

HttpProtocol(String text) {
this.text = text;
}

/**
* @return the protocol version string (e.g. "HTTP/1.1", "HTTP/2.0")
*/
public String getText() {
return text;
}

@Override
public String toString() {
return text;
}
}
9 changes: 9 additions & 0 deletions client/src/main/java/org/asynchttpclient/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,15 @@ public interface Response {
*/
boolean hasResponseBody();

/**
* Return the HTTP protocol version used for this response.
*
* @return the protocol, defaults to {@link HttpProtocol#HTTP_1_1}
*/
default HttpProtocol getProtocol() {
return HttpProtocol.HTTP_1_1;
}

/**
* Get the remote address that the client initiated the request to.
*
Expand Down
16 changes: 16 additions & 0 deletions client/src/main/java/org/asynchttpclient/netty/NettyResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import org.asynchttpclient.HttpProtocol;
import org.asynchttpclient.HttpResponseBodyPart;
import org.asynchttpclient.HttpResponseStatus;
import org.asynchttpclient.Response;
Expand Down Expand Up @@ -158,6 +159,20 @@ public List<Cookie> getCookies() {

}

@Override
public HttpProtocol getProtocol() {
if (status == null) {
return HttpProtocol.HTTP_1_1;
}
int major = status.getProtocolMajorVersion();
if (major == 2) {
return HttpProtocol.HTTP_2;
} else if (status.getProtocolMinorVersion() == 0) {
return HttpProtocol.HTTP_1_0;
}
return HttpProtocol.HTTP_1_1;
}

@Override
public boolean hasResponseStatus() {
return status != null;
Expand Down Expand Up @@ -223,6 +238,7 @@ public InputStream getResponseBodyAsStream() {
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName()).append(" {\n")
.append("\tprotocol=").append(getProtocol()).append('\n')
.append("\tstatusCode=").append(getStatusCode()).append('\n')
.append("\theaders=\n");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder;
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
import io.netty.handler.codec.http2.Http2FrameCodec;
import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
import io.netty.handler.codec.http2.Http2MultiplexHandler;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.proxy.ProxyHandler;
Expand Down Expand Up @@ -61,6 +65,7 @@
import org.asynchttpclient.netty.NettyResponseFuture;
import org.asynchttpclient.netty.OnLastHttpContentCallback;
import org.asynchttpclient.netty.handler.AsyncHttpClientHandler;
import org.asynchttpclient.netty.handler.Http2Handler;
import org.asynchttpclient.netty.handler.HttpHandler;
import org.asynchttpclient.netty.handler.WebSocketHandler;
import org.asynchttpclient.netty.request.NettyRequestSender;
Expand Down Expand Up @@ -96,6 +101,9 @@ public class ChannelManager {
public static final String AHC_HTTP_HANDLER = "ahc-http";
public static final String AHC_WS_HANDLER = "ahc-ws";
public static final String LOGGING_HANDLER = "logging";
public static final String HTTP2_FRAME_CODEC = "http2-frame-codec";
public static final String HTTP2_MULTIPLEX = "http2-multiplex";
public static final String AHC_HTTP2_HANDLER = "ahc-http2";
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelManager.class);
private final AsyncHttpClientConfig config;
private final SslEngineFactory sslEngineFactory;
Expand All @@ -109,6 +117,7 @@ public class ChannelManager {
private final ChannelGroup openChannels;

private AsyncHttpClientHandler wsHandler;
private Http2Handler http2Handler;

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

httpBootstrap.handler(new ChannelInitializer<Channel>() {
@Override
Expand Down Expand Up @@ -549,6 +559,58 @@ protected void initChannel(Channel channel) throws Exception {
return promise;
}

/**
* Checks whether the given channel is an HTTP/2 connection (i.e. has the HTTP/2 multiplex handler installed).
*/
public static boolean isHttp2(Channel channel) {
return channel.pipeline().get(HTTP2_MULTIPLEX) != null;
}

/**
* Returns the shared {@link Http2Handler} instance for use with stream child channels.
*/
public Http2Handler getHttp2Handler() {
return http2Handler;
}

/**
* Upgrades the pipeline from HTTP/1.1 to HTTP/2 after ALPN negotiates "h2".
* Removes HTTP/1.1 handlers and adds {@link Http2FrameCodec} + {@link Http2MultiplexHandler}.
* The per-stream {@link Http2Handler} is added separately on each stream child channel.
*/
public void upgradePipelineToHttp2(ChannelPipeline pipeline) {
// Remove HTTP/1.1 specific handlers
if (pipeline.get(HTTP_CLIENT_CODEC) != null) {
pipeline.remove(HTTP_CLIENT_CODEC);
}
if (pipeline.get(INFLATER_HANDLER) != null) {
pipeline.remove(INFLATER_HANDLER);
}
if (pipeline.get(CHUNKED_WRITER_HANDLER) != null) {
pipeline.remove(CHUNKED_WRITER_HANDLER);
}
if (pipeline.get(AHC_HTTP_HANDLER) != null) {
pipeline.remove(AHC_HTTP_HANDLER);
}

// Add HTTP/2 frame codec (handles connection preface, SETTINGS, PING, flow control, etc.)
Http2FrameCodec frameCodec = Http2FrameCodecBuilder.forClient()
.initialSettings(Http2Settings.defaultSettings())
.build();

// Http2MultiplexHandler creates a child channel per HTTP/2 stream.
// Server-push streams are silently ignored (no-op initializer) since AHC is client-only.
Http2MultiplexHandler multiplexHandler = new Http2MultiplexHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
// Server push not supported — ignore inbound pushed streams
}
});

pipeline.addLast(HTTP2_FRAME_CODEC, frameCodec);
pipeline.addLast(HTTP2_MULTIPLEX, multiplexHandler);
}

public void upgradePipelineForWebSockets(ChannelPipeline pipeline) {
pipeline.addAfter(HTTP_CLIENT_CODEC, WS_ENCODER_HANDLER, new WebSocket08FrameEncoder(true));
pipeline.addAfter(WS_ENCODER_HANDLER, WS_DECODER_HANDLER, new WebSocket08FrameDecoder(false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.netty.channel.Channel;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslHandler;
import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.Request;
Expand Down Expand Up @@ -185,6 +186,11 @@ protected void onSuccess(Channel value) {
NettyConnectListener.this.onFailure(channel, e);
return;
}
// Detect ALPN-negotiated protocol and upgrade pipeline to HTTP/2 if "h2" was selected
String alpnProtocol = sslHandler.applicationProtocol();
if (ApplicationProtocolNames.HTTP_2.equals(alpnProtocol)) {
channelManager.upgradePipelineToHttp2(channel.pipeline());
}
writeRequest(channel);
}

Expand Down
Loading