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 @@ -1027,10 +1027,17 @@ protected void initChannel(Channel channel) throws Exception {
}

/**
* Checks whether the given channel is an HTTP/2 connection (i.e. has the HTTP/2 multiplex handler installed).
* Checks whether the given channel is an HTTP/2 connection: the parent that multiplexes streams, not one of
* its stream children, which carry neither the multiplex handler nor connection state of their own.
* <p>
* Answered from the {@link Http2ConnectionState} attached to the connection rather than by looking
* {@link #HTTP2_MULTIPLEX} up in the pipeline. The two are attached together, in
* {@link #upgradePipelineToHttp2}, and neither is ever taken away, so they say the same thing; but a
* pipeline lookup compares handler names down the chain, and an HTTP/1.1 connection, which has no such
* handler, is walked to the end to say no. The write path asks this of every request.
*/
public static boolean isHttp2(Channel channel) {
return channel.pipeline().get(HTTP2_MULTIPLEX) != null;
return channel.attr(Http2ConnectionState.HTTP2_STATE_KEY).get() != null;
}

/**
Expand Down Expand Up @@ -1097,7 +1104,8 @@ protected void initChannel(Channel ch) {
pipeline.addLast(HTTP2_FRAME_CODEC, frameCodec);
pipeline.addLast(HTTP2_MULTIPLEX, multiplexHandler);

// Attach HTTP/2 connection state for MAX_CONCURRENT_STREAMS tracking and GOAWAY draining
// Attach HTTP/2 connection state for MAX_CONCURRENT_STREAMS tracking and GOAWAY draining. Its
// presence is also what marks the connection as HTTP/2; see isHttp2.
Http2ConnectionState state = new Http2ConnectionState();
int configMaxStreams = config.getHttp2MaxConcurrentStreams();
if (configMaxStreams > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,9 +703,12 @@ public <T> void writeRequest(NettyResponseFuture<T> future, Channel channel) {
return;
}

// Route to HTTP/2 path if the parent channel has the HTTP/2 multiplex handler installed
if (ChannelManager.isHttp2(channel)) {
writeHttp2Request(future, channel);
// Route to HTTP/2 when the connection carries HTTP/2 state, which is attached where the multiplex
// handler is. Read here rather than asked of ChannelManager.isHttp2, because the HTTP/2 path needs the
// state itself and would otherwise look up what this line has already found.
Http2ConnectionState http2State = channel.attr(Http2ConnectionState.HTTP2_STATE_KEY).get();
if (http2State != null) {
writeHttp2Request(future, channel, http2State);
return;
}

Expand Down Expand Up @@ -772,10 +775,12 @@ public <T> void writeRequest(NettyResponseFuture<T> future, Channel channel) {
* The stream child channel has the {@link org.asynchttpclient.netty.handler.Http2Handler} installed
* and the {@link NettyResponseFuture} attached to it, mirroring the HTTP/1.1 channel model.
*/
private <T> void writeHttp2Request(NettyResponseFuture<T> future, Channel parentChannel) {
Http2ConnectionState state = parentChannel.attr(Http2ConnectionState.HTTP2_STATE_KEY).get();

if (state != null && !state.tryAcquireStream()) {
/**
* @param state the connection's HTTP/2 state, which is what identified it as an HTTP/2 connection in the
* first place, so the caller has it in hand
*/
private <T> void writeHttp2Request(NettyResponseFuture<T> future, Channel parentChannel, Http2ConnectionState state) {
if (!state.tryAcquireStream()) {
if (state.isDraining()) {
// Connection is draining from GOAWAY — fail the future so it retries on a new connection.
// Don't close the parent channel since it may still have active streams. sendHttp2Frames
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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.channel;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http2.Http2StreamChannelBootstrap;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.asynchttpclient.Dsl.config;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* {@link ChannelManager#isHttp2(Channel)} answers from the {@link Http2ConnectionState} attached to a
* connection, while the thing it stands for is the multiplex handler in the pipeline. These pin the two
* together: either both say HTTP/2 or neither does, whichever way a later change to the upgrade attaches them.
*/
class ChannelManagerHttp2MarkerTest {

// One per class: the upgrade is what is under test and it needs a ChannelManager only to be called. Building
// one per test costs an SslContext and an event loop group each time, for state that lives on the channel.
private static ChannelManager channelManager;
private static Timer timer;

private EmbeddedChannel channel;

@BeforeAll
static void startManager() {
timer = new HashedWheelTimer();
channelManager = new ChannelManager(config().build(), timer);
Comment thread
pavel-ptashyts marked this conversation as resolved.
}

@AfterAll
static void stopManager() {
if (channelManager != null) {
channelManager.close();
}
if (timer != null) {
timer.stop();
}
}

@BeforeEach
void setUp() {
channel = new EmbeddedChannel();
}

@AfterEach
void tearDown() {
if (channel != null) {
channel.finishAndReleaseAll();
}
}

@Test
void aConnectionThatWasNeverUpgradedIsNotHttp2() {
assertNull(channel.pipeline().get(ChannelManager.HTTP2_MULTIPLEX),
"an untouched pipeline should not carry the multiplex handler");
assertFalse(ChannelManager.isHttp2(channel), "and should not be reported as HTTP/2");
}

@Test
void upgradingAConnectionBothInstallsTheHandlerAndReportsHttp2() {
channelManager.upgradePipelineToHttp2(channel.pipeline());

assertNotNull(channel.pipeline().get(ChannelManager.HTTP2_MULTIPLEX),
"the upgrade should install the multiplex handler");
assertTrue(ChannelManager.isHttp2(channel), "and should report the connection as HTTP/2");
}

@Test
void aStreamOfAnHttp2ConnectionIsNotTheConnection() {
// A real stream child rather than a bare channel: what is worth pinning is that a stream does not
// inherit the connection state its parent carries, since that is now what identifies an HTTP/2
// connection. The stream is where a request is written, so mistaking it for its parent would loop.
channelManager.upgradePipelineToHttp2(channel.pipeline());
channel.runPendingTasks();

Channel stream = new Http2StreamChannelBootstrap(channel)
.handler(new ChannelInboundHandlerAdapter())
.open().syncUninterruptibly().getNow();
try {
assertNull(stream.pipeline().get(ChannelManager.HTTP2_MULTIPLEX),
"a stream child carries no multiplex handler of its own");
assertFalse(ChannelManager.isHttp2(stream), "and is not the connection that multiplexes it");
} finally {
stream.close().syncUninterruptibly();
}
}
}
Loading