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 @@ -58,6 +58,7 @@
import io.netty.resolver.AddressResolver;
import io.netty.resolver.AddressResolverGroup;
import io.netty.resolver.NameResolver;
import io.netty.util.AttributeKey;
import io.netty.util.Timer;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.Future;
Expand Down Expand Up @@ -125,6 +126,8 @@ public class ChannelManager {
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";
// Set beside HTTP2_MULTIPLEX and nowhere else, so that isHttp2 can answer without a pipeline lookup.
private static final AttributeKey<Boolean> HTTP2_CONNECTION_ATTRIBUTE = AttributeKey.valueOf("http2Connection");
public static final String AHC_HTTP2_HANDLER = "ahc-http2";
private static final String TARGET_SSL_HANDLER = "target-ssl";
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelManager.class);
Expand Down Expand Up @@ -1027,10 +1030,16 @@ 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, whose own pipelines carry neither the multiplex handler nor this attribute.
* <p>
* Answered from an attribute rather than by looking {@link #HTTP2_MULTIPLEX} up in the pipeline. The two are
* set together and so always agree, 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.hasAttr(HTTP2_CONNECTION_ATTRIBUTE);
}

/**
Expand Down Expand Up @@ -1096,6 +1105,7 @@ protected void initChannel(Channel ch) {

pipeline.addLast(HTTP2_FRAME_CODEC, frameCodec);
pipeline.addLast(HTTP2_MULTIPLEX, multiplexHandler);
pipeline.channel().attr(HTTP2_CONNECTION_ATTRIBUTE).set(Boolean.TRUE);

// Attach HTTP/2 connection state for MAX_CONCURRENT_STREAMS tracking and GOAWAY draining
Http2ConnectionState state = new Http2ConnectionState();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.embedded.EmbeddedChannel;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
import org.junit.jupiter.api.AfterEach;
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(io.netty.channel.Channel)} answers from an attribute, 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 sets them.
*/
class ChannelManagerHttp2MarkerTest {

private ChannelManager channelManager;
private Timer timer;
private EmbeddedChannel channel;

@BeforeEach
void setUp() {
timer = new HashedWheelTimer();
channelManager = new ChannelManager(config().build(), timer);
channel = new EmbeddedChannel();
}

@AfterEach
void tearDown() {
channel.finishAndReleaseAll();
timer.stop();
}

@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 aStreamChannelIsNotItsParentsConnection() {
// Nothing marks a stream child, and its own pipeline carries no multiplex handler either, so the two
// agree here as well: a stream is not the connection that multiplexes it.
channelManager.upgradePipelineToHttp2(channel.pipeline());
EmbeddedChannel stream = new EmbeddedChannel();
try {
assertNull(stream.pipeline().get(ChannelManager.HTTP2_MULTIPLEX));
assertFalse(ChannelManager.isHttp2(stream));
} finally {
stream.finishAndReleaseAll();
}
}
}
Loading