From bc9ff6b9f0c3a97cec94cb10bd707a0ec6d96f8c Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Tue, 30 Jun 2026 16:38:02 +0530 Subject: [PATCH 01/14] Fix CVE-2026-47244: enforce HTTP/2 maxConcurrentStreams limit on server --- ballerina/http_service_endpoint.bal | 4 + .../stdlib/http/api/HttpConstants.java | 3 + .../ballerina/stdlib/http/api/HttpUtil.java | 3 + .../config/ListenerConfiguration.java | 9 + .../DefaultHttpWsConnectorFactory.java | 1 + .../HttpServerChannelInitializer.java | 11 +- .../listener/ServerConnectorBootstrap.java | 4 + .../Http2SourceConnectionHandlerBuilder.java | 4 +- .../http2/Http2WithPriorKnowledgeHandler.java | 8 +- .../Http2MaxConcurrentStreamsTestCase.java | 191 ++++++++++++++++++ native/src/test/resources/testng.xml | 1 + 11 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java diff --git a/ballerina/http_service_endpoint.bal b/ballerina/http_service_endpoint.bal index f41f698a4a..4bfdc78b9e 100644 --- a/ballerina/http_service_endpoint.bal +++ b/ballerina/http_service_endpoint.bal @@ -148,6 +148,9 @@ public type Local record {| # + gracefulStopTimeout - Grace period of time in seconds for listener gracefulStop # + socketConfig - Provides settings related to server socket configuration # + http2InitialWindowSize - Configuration to change the initial window size in HTTP/2 +# + http2MaxConcurrentStreams - Maximum number of concurrent HTTP/2 streams allowed per connection. Limits the +# number of active streams a single client connection can open simultaneously, +# preventing unbounded stream creation and heap exhaustion. Default is 100 # + minIdleTimeInStaleState - Minimum time in seconds for a connection to be kept open which has received a GOAWAY. # This only applies for HTTP/2. Default value is 5 minutes. If the value is set to -1, # the connection will be closed after all in-flight streams are completed @@ -164,6 +167,7 @@ public type ListenerConfiguration record {| decimal gracefulStopTimeout = DEFAULT_GRACEFULSTOP_TIMEOUT; ServerSocketConfig socketConfig = {}; int http2InitialWindowSize = 65535; + int http2MaxConcurrentStreams = 100; decimal minIdleTimeInStaleState = 300; decimal timeBetweenStaleEviction = 30; |}; diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java index 1664f88135..88edda4c39 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java @@ -444,6 +444,9 @@ public final class HttpConstants { public static final BString ENDPOINT_CONFIG_GRACEFUL_STOP_TIMEOUT = StringUtils.fromString("gracefulStopTimeout"); public static final BString ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE = StringUtils .fromString("http2InitialWindowSize"); + public static final String HTTP2_MAX_CONCURRENT_STREAMS = "http.http2.maxConcurrentStreams"; + public static final BString ENDPOINT_CONFIG_HTTP2_MAX_CONCURRENT_STREAMS = StringUtils + .fromString("http2MaxConcurrentStreams"); public static final BString ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE = StringUtils.fromString( "minIdleTimeInStaleState"); public static final BString ENDPOINT_CONFIG_TIME_BETWEEN_STALE_CHECK_RUNS = StringUtils.fromString( diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index 8c6b50a275..6f565462b6 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -134,6 +134,7 @@ import static io.ballerina.stdlib.http.api.HttpConstants.ANN_CONFIG_ATTR_SSL_ENABLED_PROTOCOLS; import static io.ballerina.stdlib.http.api.HttpConstants.CREATE_INTERCEPTORS_FUNCTION_NAME; import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE; +import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_MAX_CONCURRENT_STREAMS; import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_HEADERS; import static io.ballerina.stdlib.http.api.HttpConstants.RESOLVED_REQUESTED_URI; import static io.ballerina.stdlib.http.api.HttpConstants.RESPONSE_CACHE_CONTROL; @@ -1583,6 +1584,8 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo listenerConfiguration.setPipeliningEnabled(true); //Pipelining is enabled all the time listenerConfiguration.setHttp2InitialWindowSize(endpointConfig .getIntValue(ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE).intValue()); + listenerConfiguration.setHttp2MaxConcurrentStreams(endpointConfig + .getIntValue(ENDPOINT_CONFIG_HTTP2_MAX_CONCURRENT_STREAMS).intValue()); double minIdleTimeInStaleState = ((BDecimal) endpointConfig.get(HttpConstants.ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE)).floatValue(); diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java index 64b414a40a..cdf2e85ca1 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java @@ -64,6 +64,7 @@ public static ListenerConfiguration getDefault() { private boolean socketReuse; private boolean socketKeepAlive; private int http2InitialWindowSize = 65535; + private int http2MaxConcurrentStreams = 100; private long minIdleTimeInStaleState = 3000000; private long timeBetweenStaleEviction = 30000; @@ -285,6 +286,14 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) { this.http2InitialWindowSize = http2InitialWindowSize; } + public int getHttp2MaxConcurrentStreams() { + return http2MaxConcurrentStreams; + } + + public void setHttp2MaxConcurrentStreams(int http2MaxConcurrentStreams) { + this.http2MaxConcurrentStreams = http2MaxConcurrentStreams; + } + public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) { this.timeBetweenStaleEviction = timeBetweenStaleEviction; } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpWsConnectorFactory.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpWsConnectorFactory.java index c599668046..8d43e6f8bf 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpWsConnectorFactory.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/DefaultHttpWsConnectorFactory.java @@ -91,6 +91,7 @@ public ServerConnector createServerConnector(ServerBootstrapConfiguration server if (Constants.HTTP_2_0.equals(listenerConfig.getVersion())) { serverConnectorBootstrap.setHttp2Enabled(true); serverConnectorBootstrap.setHttp2InitialWindowSize(listenerConfig.getHttp2InitialWindowSize()); + serverConnectorBootstrap.setHttp2MaxConcurrentStreams(listenerConfig.getHttp2MaxConcurrentStreams()); serverConnectorBootstrap.setMinIdleTimeInStaleState(listenerConfig.getMinIdleTimeInStaleState()); serverConnectorBootstrap.setTimeBetweenStaleEviction(listenerConfig.getTimeBetweenStaleEviction()); } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java index 58d0ca7012..c04896a010 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java @@ -113,6 +113,7 @@ public class HttpServerChannelInitializer extends ChannelInitializer http2StaleSourceHandlers = new LinkedBlockingQueue<>(); @@ -261,7 +262,7 @@ private void configureH2cPipeline(ChannelPipeline pipeline) { // Add handler to handle http2 requests without an upgrade pipeline.addLast(new Http2WithPriorKnowledgeHandler( interfaceId, serverName, serverConnectorFuture, this, allChannels, listenerChannels, - reqSizeValidationConfig.getMaxHeaderSize(), http2InitialWindowSize)); + reqSizeValidationConfig.getMaxHeaderSize(), http2InitialWindowSize, http2MaxConcurrentStreams)); // Add http2 upgrade decoder and upgrade handler final HttpServerCodec sourceCodec = new HttpServerCodec(reqSizeValidationConfig.getMaxInitialLineLength(), reqSizeValidationConfig.getMaxHeaderSize(), @@ -281,7 +282,7 @@ private void configureH2cPipeline(ChannelPipeline pipeline) { new Http2SourceConnectionHandlerBuilder( interfaceId, serverConnectorFuture, serverName, this, this.allChannels, this.listenerChannels, reqSizeValidationConfig.getMaxHeaderSize(), - this.http2InitialWindowSize).build()); + this.http2InitialWindowSize, this.http2MaxConcurrentStreams).build()); } else { return null; } @@ -355,6 +356,10 @@ void setHttp2InitialWindowSize(int http2InitialWindowSize) { this.http2InitialWindowSize = http2InitialWindowSize; } + void setHttp2MaxConcurrentStreams(int http2MaxConcurrentStreams) { + this.http2MaxConcurrentStreams = http2MaxConcurrentStreams; + } + void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) { this.timeBetweenStaleEviction = timeBetweenStaleEviction; } @@ -447,7 +452,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { new Http2SourceConnectionHandlerBuilder( interfaceId, serverConnectorFuture, serverName, channelInitializer, allChannels, listenerChannels, reqSizeValidationConfig.getMaxHeaderSize(), - http2InitialWindowSize).build()); + http2InitialWindowSize, http2MaxConcurrentStreams).build()); } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { // handles pipeline for HTTP/1.x requests after SSL handshake configureHttpPipeline(ctx.pipeline(), Constants.HTTP_SCHEME); diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java index bc01afa92b..f2edffc12e 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java @@ -201,6 +201,10 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) { httpServerChannelInitializer.setHttp2InitialWindowSize(http2InitialWindowSize); } + public void setHttp2MaxConcurrentStreams(int http2MaxConcurrentStreams) { + httpServerChannelInitializer.setHttp2MaxConcurrentStreams(http2MaxConcurrentStreams); + } + public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) { httpServerChannelInitializer.setTimeBetweenStaleEviction(timeBetweenStaleEviction); } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java index bb6117f7fc..049e26808f 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java @@ -54,7 +54,8 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu ChannelGroup allChannels, ChannelGroup listenerChannels, long maxHeaderListSize, - int initialWindowSize) { + int initialWindowSize, + int maxConcurrentStreams) { this.interfaceId = interfaceId; this.serverConnectorFuture = serverConnectorFuture; this.serverName = serverName; @@ -63,6 +64,7 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu this.listenerChannels = listenerChannels; this.initialSettings().maxHeaderListSize(maxHeaderListSize); this.initialSettings().initialWindowSize(initialWindowSize); + this.initialSettings().maxConcurrentStreams(maxConcurrentStreams); } @Override diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java index 897a75f4e5..c278fddac0 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java @@ -49,6 +49,7 @@ public class Http2WithPriorKnowledgeHandler extends ChannelInboundHandlerAdapter private ChannelGroup listenerChannels; private long maxHeaderListSize; private int initialWindowSize; + private int maxConcurrentStreams; public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName, ServerConnectorFuture serverConnectorFuture, @@ -56,7 +57,8 @@ public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName, ChannelGroup allChannels, ChannelGroup listenerChannels, long maxHeaderListSize, - int initialWindowSize) { + int initialWindowSize, + int maxConcurrentStreams) { this.interfaceId = interfaceId; this.serverName = serverName; this.serverConnectorFuture = serverConnectorFuture; @@ -65,6 +67,7 @@ public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName, this.listenerChannels = listenerChannels; this.maxHeaderListSize = maxHeaderListSize; this.initialWindowSize = initialWindowSize; + this.maxConcurrentStreams = maxConcurrentStreams; } @Override @@ -83,7 +86,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { Constants.HTTP2_SOURCE_CONNECTION_HANDLER, new Http2SourceConnectionHandlerBuilder( interfaceId, serverConnectorFuture, serverName, serverChannelInitializer, - allChannels, listenerChannels, maxHeaderListSize, initialWindowSize).build()); + allChannels, listenerChannels, maxHeaderListSize, initialWindowSize, + maxConcurrentStreams).build()); safelyRemoveHandlers(pipeline, Constants.HTTP2_UPGRADE_HANDLER, Constants.HTTP_COMPRESSOR, Constants.HTTP_TRACE_LOG_HANDLER); } diff --git a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java new file mode 100644 index 0000000000..f6c0525716 --- /dev/null +++ b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.stdlib.http.transport.http2; + +import io.ballerina.stdlib.http.api.HttpConstants; +import io.ballerina.stdlib.http.transport.contentaware.listeners.EchoMessageListener; +import io.ballerina.stdlib.http.transport.contract.Constants; +import io.ballerina.stdlib.http.transport.contract.HttpClientConnector; +import io.ballerina.stdlib.http.transport.contract.HttpWsConnectorFactory; +import io.ballerina.stdlib.http.transport.contract.ServerConnector; +import io.ballerina.stdlib.http.transport.contract.ServerConnectorFuture; +import io.ballerina.stdlib.http.transport.contract.config.ListenerConfiguration; +import io.ballerina.stdlib.http.transport.contractimpl.DefaultHttpWsConnectorFactory; +import io.ballerina.stdlib.http.transport.message.HttpCarbonMessage; +import io.ballerina.stdlib.http.transport.message.HttpMessageDataStreamer; +import io.ballerina.stdlib.http.transport.util.TestUtil; +import io.ballerina.stdlib.http.transport.util.client.http2.MessageGenerator; +import io.ballerina.stdlib.http.transport.util.client.http2.MessageSender; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2FrameAdapter; +import io.netty.handler.codec.http2.Http2Settings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static io.ballerina.stdlib.http.transport.util.Http2Util.getTestHttp2Client; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +/** + * Tests that the HTTP/2 server advertises a bounded maxConcurrentStreams value in its initial SETTINGS frame + * and that the system property {@code http.http2.maxConcurrentStreams} can override the default. + * Verifies the fix for CVE-2026-47244 (unbounded stream creation via DefaultHttp2Connection). + */ +public class Http2MaxConcurrentStreamsTestCase { + + private static final Logger LOG = LoggerFactory.getLogger(Http2MaxConcurrentStreamsTestCase.class); + private static final int DEFAULT_MAX_CONCURRENT_STREAMS = 100; + + private ServerConnector serverConnector; + private HttpWsConnectorFactory connectorFactory; + + @BeforeClass + public void setup() throws InterruptedException { + connectorFactory = new DefaultHttpWsConnectorFactory(); + ListenerConfiguration listenerConfiguration = new ListenerConfiguration(); + listenerConfiguration.setPort(TestUtil.HTTP_SERVER_PORT); + listenerConfiguration.setScheme(Constants.HTTP_SCHEME); + listenerConfiguration.setVersion(Constants.HTTP_2_0); + serverConnector = connectorFactory.createServerConnector( + TestUtil.getDefaultServerBootstrapConfig(), listenerConfiguration); + ServerConnectorFuture future = serverConnector.start(); + future.setHttpConnectorListener(new EchoMessageListener()); + future.sync(); + } + + @Test(description = "Server must advertise maxConcurrentStreams=100 in the initial SETTINGS frame by default " + + "(CVE-2026-47244: prevents unbounded stream creation and heap exhaustion)") + public void testDefaultMaxConcurrentStreamsAdvertisedViaPriorKnowledge() throws Exception { + Long maxConcurrentStreams = captureMaxConcurrentStreamsFromSettings(TestUtil.HTTP_SERVER_PORT); + assertNotNull(maxConcurrentStreams, + "maxConcurrentStreams must be present in server SETTINGS frame"); + assertEquals((long) maxConcurrentStreams, DEFAULT_MAX_CONCURRENT_STREAMS, + "Server must advertise maxConcurrentStreams=100 by default to bound concurrent stream creation"); + } + + @Test(description = "System property http.http2.maxConcurrentStreams must override the default limit, " + + "allowing operators to restore old behaviour or set a custom bound") + public void testSystemPropertyOverridesMaxConcurrentStreams() throws Exception { + int customLimit = 50; + System.setProperty(HttpConstants.HTTP2_MAX_CONCURRENT_STREAMS, String.valueOf(customLimit)); + HttpWsConnectorFactory factory = new DefaultHttpWsConnectorFactory(); + ListenerConfiguration config = new ListenerConfiguration(); + config.setPort(TestUtil.SERVER_PORT2); + config.setScheme(Constants.HTTP_SCHEME); + config.setVersion(Constants.HTTP_2_0); + ServerConnector connector = factory.createServerConnector( + TestUtil.getDefaultServerBootstrapConfig(), config); + ServerConnectorFuture future = connector.start(); + future.setHttpConnectorListener(new EchoMessageListener()); + future.sync(); + try { + Long maxConcurrentStreams = captureMaxConcurrentStreamsFromSettings(TestUtil.SERVER_PORT2); + assertNotNull(maxConcurrentStreams, + "maxConcurrentStreams must be present in server SETTINGS frame"); + assertEquals((long) maxConcurrentStreams, customLimit, + "System property must override the default maxConcurrentStreams value"); + } finally { + System.clearProperty(HttpConstants.HTTP2_MAX_CONCURRENT_STREAMS); + connector.stop(); + try { + factory.shutdown(); + } catch (InterruptedException e) { + LOG.warn("Interrupted while shutting down factory for system property test"); + } + } + } + + @Test(description = "Normal HTTP/2 requests must succeed when the server enforces the default stream limit") + public void testRequestsSucceedWithDefaultStreamLimit() { + HttpClientConnector client = getTestHttp2Client(connectorFactory, true); + String testValue = "Test Http2 Message"; + HttpCarbonMessage request = MessageGenerator.generateRequest(HttpMethod.POST, testValue); + HttpCarbonMessage response = new MessageSender(client).sendMessage(request); + assertNotNull(response, "Expected response not received"); + String result = TestUtil.getStringFromInputStream(new HttpMessageDataStreamer(response).getInputStream()); + assertEquals(result, testValue, "Expected response payload not received"); + client.close(); + } + + @AfterClass + public void cleanUp() { + serverConnector.stop(); + try { + connectorFactory.shutdown(); + } catch (InterruptedException e) { + LOG.warn("Interrupted while waiting for HttpWsFactory to close"); + } + } + + /** + * Opens a raw HTTP/2 prior-knowledge connection to the server, waits for the server's initial SETTINGS frame, + * and returns the advertised {@code maxConcurrentStreams} value. + */ + private static Long captureMaxConcurrentStreamsFromSettings(int port) throws Exception { + CompletableFuture maxStreamsFuture = new CompletableFuture<>(); + EventLoopGroup group = new NioEventLoopGroup(1); + try { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + DefaultHttp2Connection connection = new DefaultHttp2Connection(false); + ch.pipeline().addLast(new Http2ConnectionHandlerBuilder() + .connection(connection) + .frameListener(new Http2FrameAdapter() { + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) + throws Http2Exception { + Long value = settings.maxConcurrentStreams(); + if (value != null && !maxStreamsFuture.isDone()) { + maxStreamsFuture.complete(value); + } + } + }) + .build()); + } + }); + Channel channel = bootstrap.connect(TestUtil.TEST_HOST, port).syncUninterruptibly().channel(); + Long result = maxStreamsFuture.get(5, TimeUnit.SECONDS); + channel.close().syncUninterruptibly(); + return result; + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/native/src/test/resources/testng.xml b/native/src/test/resources/testng.xml index ede152c6b3..d3e2bf1a12 100644 --- a/native/src/test/resources/testng.xml +++ b/native/src/test/resources/testng.xml @@ -132,6 +132,7 @@ + From fcb295d21208499ebf4e31459c18038dbfe74b36 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Tue, 30 Jun 2026 17:14:44 +0530 Subject: [PATCH 02/14] Document HTTP/2 maxConcurrentStreams limit and system property override --- README.md | 26 ++++++++++++++++++++++++++ ballerina/README.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/README.md b/README.md index 572d661fc8..37ddc90848 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,32 @@ built-in support for basic authentication, JWT authentication, and OAuth2 authen In addition to that, supports both the HTTP/1.1 and HTTP2 protocols and connection keep-alive, content chunking, HTTP caching, data compression/decompression, payload binding, and authorization can be highlighted as the features of a `Service`. +#### HTTP/2 Stream Concurrency + +Each HTTP/2 connection can carry multiple requests simultaneously using independent streams. To prevent a single +connection from opening an unbounded number of streams and exhausting server memory, the listener enforces a limit +of **100 concurrent streams per connection** by default. This is the value recommended by RFC 7540 and consistent +with other widely-used HTTP/2 server implementations such as Nginx and Envoy. + +When a client reaches this limit on a connection, it will automatically open an additional connection rather than +stalling, so normal traffic is unaffected for typical workloads. + +The limit can be configured per listener using the `http2MaxConcurrentStreams` field in `ListenerConfiguration`: + +```ballerina +listener http:Listener secureHelloWorldEp = new (9090, { + http2MaxConcurrentStreams: 500 +}); +``` + +Alternatively, you can override the default globally for all listeners using the following JVM system property: + +``` +JAVA_OPTS="-Dhttp.http2.maxConcurrentStreams=500" bal run +``` + +> **Note:** The per-listener `http2MaxConcurrentStreams` configuration takes precedence over the system property. + ## Issues and projects Issues and Projects tabs are disabled for this repository as this is part of the Ballerina Standard Library. To report bugs, request new features, start new discussions, view project boards, etc. please visit Ballerina Standard Library [parent repository](https://github.com/ballerina-platform/ballerina-standard-library). diff --git a/ballerina/README.md b/ballerina/README.md index 3b590b3113..382d703f50 100644 --- a/ballerina/README.md +++ b/ballerina/README.md @@ -97,3 +97,35 @@ built-in support for basic authentication, JWT authentication, and OAuth2 authen In addition to that, supports both the HTTP/1.1 and HTTP2 protocols and connection keep-alive, content chunking, HTTP caching, data compression/decompression, payload binding, and authorization can be highlighted as the features of a `Service`. + +#### HTTP/2 Stream Concurrency + +Each HTTP/2 connection can carry multiple requests simultaneously using independent streams. To prevent a single +connection from opening an unbounded number of streams and exhausting server memory, the listener enforces a limit +of **100 concurrent streams per connection** by default. This is the value recommended by RFC 7540 and consistent +with other widely-used HTTP/2 server implementations such as Nginx and Envoy. + +When a client reaches this limit on a connection, it will automatically open an additional connection rather than +stalling, so normal traffic is unaffected for typical workloads. + +The limit can be configured per listener using the `http2MaxConcurrentStreams` field in `ListenerConfiguration`: + +```ballerina +listener http:Listener secureHelloWorldEp = new (9090, { + http2MaxConcurrentStreams: 500 +}); +``` + +Alternatively, you can override the default globally for all listeners using the following JVM system property: + +``` +-Dhttp.http2.maxConcurrentStreams= +``` + +For example: + +``` +JAVA_OPTS="-Dhttp.http2.maxConcurrentStreams=500" bal run +``` + +> **Note:** The per-listener `http2MaxConcurrentStreams` configuration takes precedence over the system property. From 06a6e2b050523743e93ce9b52bee33cf2e1f115d Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Tue, 30 Jun 2026 18:38:48 +0530 Subject: [PATCH 03/14] Fix test to use ListenerConfiguration instead of system property for master branch --- .../http2/Http2MaxConcurrentStreamsTestCase.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java index f6c0525716..8822bb785f 100644 --- a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java +++ b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java @@ -18,7 +18,6 @@ package io.ballerina.stdlib.http.transport.http2; -import io.ballerina.stdlib.http.api.HttpConstants; import io.ballerina.stdlib.http.transport.contentaware.listeners.EchoMessageListener; import io.ballerina.stdlib.http.transport.contract.Constants; import io.ballerina.stdlib.http.transport.contract.HttpClientConnector; @@ -96,16 +95,16 @@ public void testDefaultMaxConcurrentStreamsAdvertisedViaPriorKnowledge() throws "Server must advertise maxConcurrentStreams=100 by default to bound concurrent stream creation"); } - @Test(description = "System property http.http2.maxConcurrentStreams must override the default limit, " - + "allowing operators to restore old behaviour or set a custom bound") - public void testSystemPropertyOverridesMaxConcurrentStreams() throws Exception { + @Test(description = "ListenerConfiguration.http2MaxConcurrentStreams must override the default limit per listener, " + + "allowing per-listener tuning of concurrent stream capacity") + public void testListenerConfigOverridesMaxConcurrentStreams() throws Exception { int customLimit = 50; - System.setProperty(HttpConstants.HTTP2_MAX_CONCURRENT_STREAMS, String.valueOf(customLimit)); HttpWsConnectorFactory factory = new DefaultHttpWsConnectorFactory(); ListenerConfiguration config = new ListenerConfiguration(); config.setPort(TestUtil.SERVER_PORT2); config.setScheme(Constants.HTTP_SCHEME); config.setVersion(Constants.HTTP_2_0); + config.setHttp2MaxConcurrentStreams(customLimit); ServerConnector connector = factory.createServerConnector( TestUtil.getDefaultServerBootstrapConfig(), config); ServerConnectorFuture future = connector.start(); @@ -116,14 +115,13 @@ public void testSystemPropertyOverridesMaxConcurrentStreams() throws Exception { assertNotNull(maxConcurrentStreams, "maxConcurrentStreams must be present in server SETTINGS frame"); assertEquals((long) maxConcurrentStreams, customLimit, - "System property must override the default maxConcurrentStreams value"); + "ListenerConfiguration must override the default maxConcurrentStreams value"); } finally { - System.clearProperty(HttpConstants.HTTP2_MAX_CONCURRENT_STREAMS); connector.stop(); try { factory.shutdown(); } catch (InterruptedException e) { - LOG.warn("Interrupted while shutting down factory for system property test"); + LOG.warn("Interrupted while shutting down factory for listener config test"); } } } From d2bf76bef2b5110b38b4ded48301c38e00d2aee5 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 11:17:09 +0530 Subject: [PATCH 04/14] Rework CVE-2026-47244 fix to derive maxActiveStreams from global pool config --- README.md | 19 ++-- ballerina/README.md | 25 ++--- ballerina/http_service_endpoint.bal | 4 - .../stdlib/http/api/HttpConstants.java | 3 - .../ballerina/stdlib/http/api/HttpUtil.java | 14 ++- .../api/client/endpoint/InitGlobalPool.java | 2 + .../config/ListenerConfiguration.java | 8 +- .../HttpServerChannelInitializer.java | 12 +- .../listener/ServerConnectorBootstrap.java | 4 +- .../Http2SourceConnectionHandlerBuilder.java | 4 +- .../http2/Http2WithPriorKnowledgeHandler.java | 8 +- .../Http2MaxConcurrentStreamsTestCase.java | 105 ++++++------------ 12 files changed, 81 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 37ddc90848..ee0a61dab1 100644 --- a/README.md +++ b/README.md @@ -117,21 +117,20 @@ with other widely-used HTTP/2 server implementations such as Nginx and Envoy. When a client reaches this limit on a connection, it will automatically open an additional connection rather than stalling, so normal traffic is unaffected for typical workloads. -The limit can be configured per listener using the `http2MaxConcurrentStreams` field in `ListenerConfiguration`: +The server-side stream limit is derived from the `maxActiveStreamsPerConnection` configurable, which also controls +how many parallel streams the HTTP client opens per connection. Set it in `Config.toml`: -```ballerina -listener http:Listener secureHelloWorldEp = new (9090, { - http2MaxConcurrentStreams: 500 -}); +```toml +[ballerina.http] +maxActiveStreamsPerConnection = 500 ``` -Alternatively, you can override the default globally for all listeners using the following JVM system property: +To disable the limit and restore unlimited behaviour (not recommended for public-facing services): +```toml +[ballerina.http] +maxActiveStreamsPerConnection = -1 ``` -JAVA_OPTS="-Dhttp.http2.maxConcurrentStreams=500" bal run -``` - -> **Note:** The per-listener `http2MaxConcurrentStreams` configuration takes precedence over the system property. ## Issues and projects diff --git a/ballerina/README.md b/ballerina/README.md index 382d703f50..0887404356 100644 --- a/ballerina/README.md +++ b/ballerina/README.md @@ -108,24 +108,17 @@ with other widely-used HTTP/2 server implementations such as Nginx and Envoy. When a client reaches this limit on a connection, it will automatically open an additional connection rather than stalling, so normal traffic is unaffected for typical workloads. -The limit can be configured per listener using the `http2MaxConcurrentStreams` field in `ListenerConfiguration`: +The server-side stream limit is derived from the `maxActiveStreamsPerConnection` configurable, which also controls +how many parallel streams the HTTP client opens per connection. Set it in `Config.toml`: -```ballerina -listener http:Listener secureHelloWorldEp = new (9090, { - http2MaxConcurrentStreams: 500 -}); +```toml +[ballerina.http] +maxActiveStreamsPerConnection = 500 ``` -Alternatively, you can override the default globally for all listeners using the following JVM system property: +To disable the limit and restore unlimited behaviour (not recommended for public-facing services): +```toml +[ballerina.http] +maxActiveStreamsPerConnection = -1 ``` --Dhttp.http2.maxConcurrentStreams= -``` - -For example: - -``` -JAVA_OPTS="-Dhttp.http2.maxConcurrentStreams=500" bal run -``` - -> **Note:** The per-listener `http2MaxConcurrentStreams` configuration takes precedence over the system property. diff --git a/ballerina/http_service_endpoint.bal b/ballerina/http_service_endpoint.bal index 4bfdc78b9e..f41f698a4a 100644 --- a/ballerina/http_service_endpoint.bal +++ b/ballerina/http_service_endpoint.bal @@ -148,9 +148,6 @@ public type Local record {| # + gracefulStopTimeout - Grace period of time in seconds for listener gracefulStop # + socketConfig - Provides settings related to server socket configuration # + http2InitialWindowSize - Configuration to change the initial window size in HTTP/2 -# + http2MaxConcurrentStreams - Maximum number of concurrent HTTP/2 streams allowed per connection. Limits the -# number of active streams a single client connection can open simultaneously, -# preventing unbounded stream creation and heap exhaustion. Default is 100 # + minIdleTimeInStaleState - Minimum time in seconds for a connection to be kept open which has received a GOAWAY. # This only applies for HTTP/2. Default value is 5 minutes. If the value is set to -1, # the connection will be closed after all in-flight streams are completed @@ -167,7 +164,6 @@ public type ListenerConfiguration record {| decimal gracefulStopTimeout = DEFAULT_GRACEFULSTOP_TIMEOUT; ServerSocketConfig socketConfig = {}; int http2InitialWindowSize = 65535; - int http2MaxConcurrentStreams = 100; decimal minIdleTimeInStaleState = 300; decimal timeBetweenStaleEviction = 30; |}; diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java index 88edda4c39..1664f88135 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java @@ -444,9 +444,6 @@ public final class HttpConstants { public static final BString ENDPOINT_CONFIG_GRACEFUL_STOP_TIMEOUT = StringUtils.fromString("gracefulStopTimeout"); public static final BString ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE = StringUtils .fromString("http2InitialWindowSize"); - public static final String HTTP2_MAX_CONCURRENT_STREAMS = "http.http2.maxConcurrentStreams"; - public static final BString ENDPOINT_CONFIG_HTTP2_MAX_CONCURRENT_STREAMS = StringUtils - .fromString("http2MaxConcurrentStreams"); public static final BString ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE = StringUtils.fromString( "minIdleTimeInStaleState"); public static final BString ENDPOINT_CONFIG_TIME_BETWEEN_STALE_CHECK_RUNS = StringUtils.fromString( diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index 6f565462b6..eb34b1bca5 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -134,7 +134,6 @@ import static io.ballerina.stdlib.http.api.HttpConstants.ANN_CONFIG_ATTR_SSL_ENABLED_PROTOCOLS; import static io.ballerina.stdlib.http.api.HttpConstants.CREATE_INTERCEPTORS_FUNCTION_NAME; import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE; -import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_MAX_CONCURRENT_STREAMS; import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_HEADERS; import static io.ballerina.stdlib.http.api.HttpConstants.RESOLVED_REQUESTED_URI; import static io.ballerina.stdlib.http.api.HttpConstants.RESPONSE_CACHE_CONTROL; @@ -209,6 +208,8 @@ public class HttpUtil { private static final String[] DEFAULT_NAMED_GROUPS = { "X25519MLKEM768", "x25519", "secp256r1", "secp384r1", "secp521r1" }; + private static volatile int globalHttp2MaxActiveStreams = 100; + /** * Set new entity to in/out request/response struct. * @@ -1338,6 +1339,14 @@ public static ConnectionManager getConnectionManager(BMap poolStruct) { return poolManager; } + public static void setGlobalHttp2MaxActiveStreams(int maxActiveStreams) { + globalHttp2MaxActiveStreams = maxActiveStreams; + } + + public static int getGlobalHttp2MaxActiveStreams() { + return globalHttp2MaxActiveStreams; + } + public static void populatePoolingConfig(BMap poolRecord, PoolConfiguration poolConfiguration) { long maxActiveConnections = poolRecord.getIntValue(HttpConstants.CONNECTION_POOLING_MAX_ACTIVE_CONNECTIONS); poolConfiguration.setMaxActivePerPool( @@ -1584,8 +1593,7 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo listenerConfiguration.setPipeliningEnabled(true); //Pipelining is enabled all the time listenerConfiguration.setHttp2InitialWindowSize(endpointConfig .getIntValue(ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE).intValue()); - listenerConfiguration.setHttp2MaxConcurrentStreams(endpointConfig - .getIntValue(ENDPOINT_CONFIG_HTTP2_MAX_CONCURRENT_STREAMS).intValue()); + listenerConfiguration.setHttp2MaxActiveStreams(getGlobalHttp2MaxActiveStreams()); double minIdleTimeInStaleState = ((BDecimal) endpointConfig.get(HttpConstants.ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE)).floatValue(); diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java index 0ff42a2984..e065848089 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java @@ -19,6 +19,7 @@ package io.ballerina.stdlib.http.api.client.endpoint; import io.ballerina.runtime.api.values.BMap; +import io.ballerina.stdlib.http.api.HttpUtil; import io.ballerina.stdlib.http.transport.contractimpl.sender.channel.pool.ConnectionManager; import io.ballerina.stdlib.http.transport.contractimpl.sender.channel.pool.PoolConfiguration; @@ -34,6 +35,7 @@ public class InitGlobalPool { public static void initGlobalPool(BMap globalPoolConfig) { PoolConfiguration globalPool = new PoolConfiguration(); populatePoolingConfig(globalPoolConfig, globalPool); + HttpUtil.setGlobalHttp2MaxActiveStreams(globalPool.getHttp2MaxActiveStreamsPerConnection()); ConnectionManager connectionManager = new ConnectionManager(globalPool); globalPoolConfig.addNativeData(CONNECTION_MANAGER, connectionManager); } diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java index cdf2e85ca1..de4b3e69fc 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java @@ -64,7 +64,7 @@ public static ListenerConfiguration getDefault() { private boolean socketReuse; private boolean socketKeepAlive; private int http2InitialWindowSize = 65535; - private int http2MaxConcurrentStreams = 100; + private int http2MaxActiveStreams = 100; private long minIdleTimeInStaleState = 3000000; private long timeBetweenStaleEviction = 30000; @@ -287,11 +287,11 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) { } public int getHttp2MaxConcurrentStreams() { - return http2MaxConcurrentStreams; + return http2MaxActiveStreams; } - public void setHttp2MaxConcurrentStreams(int http2MaxConcurrentStreams) { - this.http2MaxConcurrentStreams = http2MaxConcurrentStreams; + public void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) { + this.http2MaxActiveStreams = http2MaxActiveStreams; } public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) { diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java index c04896a010..f2b9e7a45f 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/HttpServerChannelInitializer.java @@ -113,7 +113,7 @@ public class HttpServerChannelInitializer extends ChannelInitializer http2StaleSourceHandlers = new LinkedBlockingQueue<>(); @@ -262,7 +262,7 @@ private void configureH2cPipeline(ChannelPipeline pipeline) { // Add handler to handle http2 requests without an upgrade pipeline.addLast(new Http2WithPriorKnowledgeHandler( interfaceId, serverName, serverConnectorFuture, this, allChannels, listenerChannels, - reqSizeValidationConfig.getMaxHeaderSize(), http2InitialWindowSize, http2MaxConcurrentStreams)); + reqSizeValidationConfig.getMaxHeaderSize(), http2InitialWindowSize, http2MaxActiveStreams)); // Add http2 upgrade decoder and upgrade handler final HttpServerCodec sourceCodec = new HttpServerCodec(reqSizeValidationConfig.getMaxInitialLineLength(), reqSizeValidationConfig.getMaxHeaderSize(), @@ -282,7 +282,7 @@ private void configureH2cPipeline(ChannelPipeline pipeline) { new Http2SourceConnectionHandlerBuilder( interfaceId, serverConnectorFuture, serverName, this, this.allChannels, this.listenerChannels, reqSizeValidationConfig.getMaxHeaderSize(), - this.http2InitialWindowSize, this.http2MaxConcurrentStreams).build()); + this.http2InitialWindowSize, this.http2MaxActiveStreams).build()); } else { return null; } @@ -356,8 +356,8 @@ void setHttp2InitialWindowSize(int http2InitialWindowSize) { this.http2InitialWindowSize = http2InitialWindowSize; } - void setHttp2MaxConcurrentStreams(int http2MaxConcurrentStreams) { - this.http2MaxConcurrentStreams = http2MaxConcurrentStreams; + void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) { + this.http2MaxActiveStreams = http2MaxActiveStreams; } void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) { @@ -452,7 +452,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { new Http2SourceConnectionHandlerBuilder( interfaceId, serverConnectorFuture, serverName, channelInitializer, allChannels, listenerChannels, reqSizeValidationConfig.getMaxHeaderSize(), - http2InitialWindowSize, http2MaxConcurrentStreams).build()); + http2InitialWindowSize, http2MaxActiveStreams).build()); } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { // handles pipeline for HTTP/1.x requests after SSL handshake configureHttpPipeline(ctx.pipeline(), Constants.HTTP_SCHEME); diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java index f2edffc12e..8f7a5dbd63 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/ServerConnectorBootstrap.java @@ -201,8 +201,8 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) { httpServerChannelInitializer.setHttp2InitialWindowSize(http2InitialWindowSize); } - public void setHttp2MaxConcurrentStreams(int http2MaxConcurrentStreams) { - httpServerChannelInitializer.setHttp2MaxConcurrentStreams(http2MaxConcurrentStreams); + public void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) { + httpServerChannelInitializer.setHttp2MaxConcurrentStreams(http2MaxActiveStreams); } public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) { diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java index 049e26808f..c902172339 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java @@ -55,7 +55,7 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu ChannelGroup listenerChannels, long maxHeaderListSize, int initialWindowSize, - int maxConcurrentStreams) { + int maxActiveStreams) { this.interfaceId = interfaceId; this.serverConnectorFuture = serverConnectorFuture; this.serverName = serverName; @@ -64,7 +64,7 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu this.listenerChannels = listenerChannels; this.initialSettings().maxHeaderListSize(maxHeaderListSize); this.initialSettings().initialWindowSize(initialWindowSize); - this.initialSettings().maxConcurrentStreams(maxConcurrentStreams); + this.initialSettings().maxActiveStreams(maxActiveStreams); } @Override diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java index c278fddac0..70eca2a85d 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2WithPriorKnowledgeHandler.java @@ -49,7 +49,7 @@ public class Http2WithPriorKnowledgeHandler extends ChannelInboundHandlerAdapter private ChannelGroup listenerChannels; private long maxHeaderListSize; private int initialWindowSize; - private int maxConcurrentStreams; + private int maxActiveStreams; public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName, ServerConnectorFuture serverConnectorFuture, @@ -58,7 +58,7 @@ public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName, ChannelGroup listenerChannels, long maxHeaderListSize, int initialWindowSize, - int maxConcurrentStreams) { + int maxActiveStreams) { this.interfaceId = interfaceId; this.serverName = serverName; this.serverConnectorFuture = serverConnectorFuture; @@ -67,7 +67,7 @@ public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName, this.listenerChannels = listenerChannels; this.maxHeaderListSize = maxHeaderListSize; this.initialWindowSize = initialWindowSize; - this.maxConcurrentStreams = maxConcurrentStreams; + this.maxActiveStreams = maxActiveStreams; } @Override @@ -87,7 +87,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { new Http2SourceConnectionHandlerBuilder( interfaceId, serverConnectorFuture, serverName, serverChannelInitializer, allChannels, listenerChannels, maxHeaderListSize, initialWindowSize, - maxConcurrentStreams).build()); + maxActiveStreams).build()); safelyRemoveHandlers(pipeline, Constants.HTTP2_UPGRADE_HANDLER, Constants.HTTP_COMPRESSOR, Constants.HTTP_TRACE_LOG_HANDLER); } diff --git a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java index 8822bb785f..969e2f2807 100644 --- a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java +++ b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java @@ -20,17 +20,12 @@ import io.ballerina.stdlib.http.transport.contentaware.listeners.EchoMessageListener; import io.ballerina.stdlib.http.transport.contract.Constants; -import io.ballerina.stdlib.http.transport.contract.HttpClientConnector; import io.ballerina.stdlib.http.transport.contract.HttpWsConnectorFactory; import io.ballerina.stdlib.http.transport.contract.ServerConnector; import io.ballerina.stdlib.http.transport.contract.ServerConnectorFuture; import io.ballerina.stdlib.http.transport.contract.config.ListenerConfiguration; import io.ballerina.stdlib.http.transport.contractimpl.DefaultHttpWsConnectorFactory; -import io.ballerina.stdlib.http.transport.message.HttpCarbonMessage; -import io.ballerina.stdlib.http.transport.message.HttpMessageDataStreamer; import io.ballerina.stdlib.http.transport.util.TestUtil; -import io.ballerina.stdlib.http.transport.util.client.http2.MessageGenerator; -import io.ballerina.stdlib.http.transport.util.client.http2.MessageSender; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; @@ -39,7 +34,6 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.Http2Exception; @@ -47,37 +41,35 @@ import io.netty.handler.codec.http2.Http2Settings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; +import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import static io.ballerina.stdlib.http.transport.util.Http2Util.getTestHttp2Client; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; /** - * Tests that the HTTP/2 server advertises a bounded maxConcurrentStreams value in its initial SETTINGS frame - * and that the system property {@code http.http2.maxConcurrentStreams} can override the default. - * Verifies the fix for CVE-2026-47244 (unbounded stream creation via DefaultHttp2Connection). + * Tests that the HTTP/2 server advertises the configured {@code SETTINGS_MAX_CONCURRENT_STREAMS} + * value (CVE-2026-47244). A finite limit (default 100, sourced from maxActiveStreamsPerConnection) + * prevents unbounded stream creation; the unlimited case (Integer.MAX_VALUE) preserves legacy + * behaviour for deployments that configured {@code maxActiveStreamsPerConnection = -1}. */ public class Http2MaxConcurrentStreamsTestCase { private static final Logger LOG = LoggerFactory.getLogger(Http2MaxConcurrentStreamsTestCase.class); - private static final int DEFAULT_MAX_CONCURRENT_STREAMS = 100; private ServerConnector serverConnector; private HttpWsConnectorFactory connectorFactory; - @BeforeClass - public void setup() throws InterruptedException { + private void startServer(int port, int maxActiveStreams) throws InterruptedException { connectorFactory = new DefaultHttpWsConnectorFactory(); ListenerConfiguration listenerConfiguration = new ListenerConfiguration(); - listenerConfiguration.setPort(TestUtil.HTTP_SERVER_PORT); + listenerConfiguration.setPort(port); listenerConfiguration.setScheme(Constants.HTTP_SCHEME); listenerConfiguration.setVersion(Constants.HTTP_2_0); + listenerConfiguration.setHttp2MaxActiveStreams(maxActiveStreams); serverConnector = connectorFactory.createServerConnector( TestUtil.getDefaultServerBootstrapConfig(), listenerConfiguration); ServerConnectorFuture future = serverConnector.start(); @@ -85,73 +77,40 @@ public void setup() throws InterruptedException { future.sync(); } - @Test(description = "Server must advertise maxConcurrentStreams=100 in the initial SETTINGS frame by default " + @Test(description = "Server must advertise the configured finite limit in the initial SETTINGS frame " + "(CVE-2026-47244: prevents unbounded stream creation and heap exhaustion)") - public void testDefaultMaxConcurrentStreamsAdvertisedViaPriorKnowledge() throws Exception { + public void testServerAdvertisesConfiguredMaxConcurrentStreams() throws Exception { + startServer(TestUtil.HTTP_SERVER_PORT, 100); Long maxConcurrentStreams = captureMaxConcurrentStreamsFromSettings(TestUtil.HTTP_SERVER_PORT); - assertNotNull(maxConcurrentStreams, - "maxConcurrentStreams must be present in server SETTINGS frame"); - assertEquals((long) maxConcurrentStreams, DEFAULT_MAX_CONCURRENT_STREAMS, - "Server must advertise maxConcurrentStreams=100 by default to bound concurrent stream creation"); + assertNotNull(maxConcurrentStreams, "maxConcurrentStreams must be present in server SETTINGS frame"); + assertEquals((long) maxConcurrentStreams, 100L, + "Server must advertise the configured SETTINGS_MAX_CONCURRENT_STREAMS"); } - @Test(description = "ListenerConfiguration.http2MaxConcurrentStreams must override the default limit per listener, " - + "allowing per-listener tuning of concurrent stream capacity") - public void testListenerConfigOverridesMaxConcurrentStreams() throws Exception { - int customLimit = 50; - HttpWsConnectorFactory factory = new DefaultHttpWsConnectorFactory(); - ListenerConfiguration config = new ListenerConfiguration(); - config.setPort(TestUtil.SERVER_PORT2); - config.setScheme(Constants.HTTP_SCHEME); - config.setVersion(Constants.HTTP_2_0); - config.setHttp2MaxConcurrentStreams(customLimit); - ServerConnector connector = factory.createServerConnector( - TestUtil.getDefaultServerBootstrapConfig(), config); - ServerConnectorFuture future = connector.start(); - future.setHttpConnectorListener(new EchoMessageListener()); - future.sync(); - try { - Long maxConcurrentStreams = captureMaxConcurrentStreamsFromSettings(TestUtil.SERVER_PORT2); - assertNotNull(maxConcurrentStreams, - "maxConcurrentStreams must be present in server SETTINGS frame"); - assertEquals((long) maxConcurrentStreams, customLimit, - "ListenerConfiguration must override the default maxConcurrentStreams value"); - } finally { - connector.stop(); - try { - factory.shutdown(); - } catch (InterruptedException e) { - LOG.warn("Interrupted while shutting down factory for listener config test"); - } - } + @Test(description = "Unlimited (Integer.MAX_VALUE) advertises an effectively unbounded limit, " + + "overriding Netty's default of 100 to preserve legacy behaviour") + public void testServerAdvertisesUnlimitedMaxConcurrentStreams() throws Exception { + startServer(TestUtil.SERVER_PORT2, Integer.MAX_VALUE); + Long maxConcurrentStreams = captureMaxConcurrentStreamsFromSettings(TestUtil.SERVER_PORT2); + assertNotNull(maxConcurrentStreams, "maxConcurrentStreams must be present in server SETTINGS frame"); + assertEquals((long) maxConcurrentStreams, (long) Integer.MAX_VALUE, + "Server must advertise an effectively unbounded limit when configured as unlimited"); } - @Test(description = "Normal HTTP/2 requests must succeed when the server enforces the default stream limit") - public void testRequestsSucceedWithDefaultStreamLimit() { - HttpClientConnector client = getTestHttp2Client(connectorFactory, true); - String testValue = "Test Http2 Message"; - HttpCarbonMessage request = MessageGenerator.generateRequest(HttpMethod.POST, testValue); - HttpCarbonMessage response = new MessageSender(client).sendMessage(request); - assertNotNull(response, "Expected response not received"); - String result = TestUtil.getStringFromInputStream(new HttpMessageDataStreamer(response).getInputStream()); - assertEquals(result, testValue, "Expected response payload not received"); - client.close(); - } - - @AfterClass + @AfterMethod public void cleanUp() { - serverConnector.stop(); - try { - connectorFactory.shutdown(); - } catch (InterruptedException e) { - LOG.warn("Interrupted while waiting for HttpWsFactory to close"); + if (serverConnector != null) { + serverConnector.stop(); + } + if (connectorFactory != null) { + try { + connectorFactory.shutdown(); + } catch (InterruptedException e) { + LOG.warn("Interrupted while waiting for HttpWsFactory to close"); + } } } - /** - * Opens a raw HTTP/2 prior-knowledge connection to the server, waits for the server's initial SETTINGS frame, - * and returns the advertised {@code maxConcurrentStreams} value. - */ private static Long captureMaxConcurrentStreamsFromSettings(int port) throws Exception { CompletableFuture maxStreamsFuture = new CompletableFuture<>(); EventLoopGroup group = new NioEventLoopGroup(1); From d4c199563648b8599f48f62ecaef53ed3828c35c Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 11:47:10 +0530 Subject: [PATCH 05/14] Add optional http2MaxActiveStreams field to ListenerConfiguration --- README.md | 16 ++++++++++------ ballerina/README.md | 16 ++++++++++------ ballerina/http_service_endpoint.bal | 5 +++++ .../ballerina/stdlib/http/api/HttpConstants.java | 2 ++ .../io/ballerina/stdlib/http/api/HttpUtil.java | 9 ++++++++- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ee0a61dab1..af6f6613c5 100644 --- a/README.md +++ b/README.md @@ -117,21 +117,25 @@ with other widely-used HTTP/2 server implementations such as Nginx and Envoy. When a client reaches this limit on a connection, it will automatically open an additional connection rather than stalling, so normal traffic is unaffected for typical workloads. -The server-side stream limit is derived from the `maxActiveStreamsPerConnection` configurable, which also controls -how many parallel streams the HTTP client opens per connection. Set it in `Config.toml`: +The server-side stream limit defaults to the value of the `maxActiveStreamsPerConnection` configurable (default 100), +which also controls how many parallel streams the HTTP client opens per connection. Set it globally in `Config.toml`: ```toml [ballerina.http] maxActiveStreamsPerConnection = 500 ``` -To disable the limit and restore unlimited behaviour (not recommended for public-facing services): +You can also override it per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: -```toml -[ballerina.http] -maxActiveStreamsPerConnection = -1 +```ballerina +listener http:Listener h2Listener = new (9090, { + http2MaxActiveStreams: 500 +}); ``` +Set to `-1` to allow unlimited streams (not recommended for public-facing services). When not set, the listener +inherits the global `maxActiveStreamsPerConnection` value. + ## Issues and projects Issues and Projects tabs are disabled for this repository as this is part of the Ballerina Standard Library. To report bugs, request new features, start new discussions, view project boards, etc. please visit Ballerina Standard Library [parent repository](https://github.com/ballerina-platform/ballerina-standard-library). diff --git a/ballerina/README.md b/ballerina/README.md index 0887404356..7742ec8483 100644 --- a/ballerina/README.md +++ b/ballerina/README.md @@ -108,17 +108,21 @@ with other widely-used HTTP/2 server implementations such as Nginx and Envoy. When a client reaches this limit on a connection, it will automatically open an additional connection rather than stalling, so normal traffic is unaffected for typical workloads. -The server-side stream limit is derived from the `maxActiveStreamsPerConnection` configurable, which also controls -how many parallel streams the HTTP client opens per connection. Set it in `Config.toml`: +The server-side stream limit defaults to the value of the `maxActiveStreamsPerConnection` configurable (default 100), +which also controls how many parallel streams the HTTP client opens per connection. Set it globally in `Config.toml`: ```toml [ballerina.http] maxActiveStreamsPerConnection = 500 ``` -To disable the limit and restore unlimited behaviour (not recommended for public-facing services): +You can also override it per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: -```toml -[ballerina.http] -maxActiveStreamsPerConnection = -1 +```ballerina +listener http:Listener h2Listener = new (9090, { + http2MaxActiveStreams: 500 +}); ``` + +Set to `-1` to allow unlimited streams (not recommended for public-facing services). When not set, the listener +inherits the global `maxActiveStreamsPerConnection` value. diff --git a/ballerina/http_service_endpoint.bal b/ballerina/http_service_endpoint.bal index f41f698a4a..7f0f50be59 100644 --- a/ballerina/http_service_endpoint.bal +++ b/ballerina/http_service_endpoint.bal @@ -148,6 +148,10 @@ public type Local record {| # + gracefulStopTimeout - Grace period of time in seconds for listener gracefulStop # + socketConfig - Provides settings related to server socket configuration # + http2InitialWindowSize - Configuration to change the initial window size in HTTP/2 +# + http2MaxActiveStreams - Maximum concurrent HTTP/2 streams per connection advertised to clients. When set, +# overrides the global `maxActiveStreamsPerConnection` configurable for this listener. +# Set to -1 for unlimited streams. When not set, inherits from the global +# `maxActiveStreamsPerConnection` configurable (itself defaulting to 100) # + minIdleTimeInStaleState - Minimum time in seconds for a connection to be kept open which has received a GOAWAY. # This only applies for HTTP/2. Default value is 5 minutes. If the value is set to -1, # the connection will be closed after all in-flight streams are completed @@ -164,6 +168,7 @@ public type ListenerConfiguration record {| decimal gracefulStopTimeout = DEFAULT_GRACEFULSTOP_TIMEOUT; ServerSocketConfig socketConfig = {}; int http2InitialWindowSize = 65535; + int? http2MaxActiveStreams = (); decimal minIdleTimeInStaleState = 300; decimal timeBetweenStaleEviction = 30; |}; diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java index 1664f88135..2807120c03 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java @@ -444,6 +444,8 @@ public final class HttpConstants { public static final BString ENDPOINT_CONFIG_GRACEFUL_STOP_TIMEOUT = StringUtils.fromString("gracefulStopTimeout"); public static final BString ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE = StringUtils .fromString("http2InitialWindowSize"); + public static final BString ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS = StringUtils + .fromString("http2MaxActiveStreams"); public static final BString ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE = StringUtils.fromString( "minIdleTimeInStaleState"); public static final BString ENDPOINT_CONFIG_TIME_BETWEEN_STALE_CHECK_RUNS = StringUtils.fromString( diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index eb34b1bca5..1c1d9edb1a 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -134,6 +134,7 @@ import static io.ballerina.stdlib.http.api.HttpConstants.ANN_CONFIG_ATTR_SSL_ENABLED_PROTOCOLS; import static io.ballerina.stdlib.http.api.HttpConstants.CREATE_INTERCEPTORS_FUNCTION_NAME; import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE; +import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS; import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_HEADERS; import static io.ballerina.stdlib.http.api.HttpConstants.RESOLVED_REQUESTED_URI; import static io.ballerina.stdlib.http.api.HttpConstants.RESPONSE_CACHE_CONTROL; @@ -1593,7 +1594,13 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo listenerConfiguration.setPipeliningEnabled(true); //Pipelining is enabled all the time listenerConfiguration.setHttp2InitialWindowSize(endpointConfig .getIntValue(ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE).intValue()); - listenerConfiguration.setHttp2MaxActiveStreams(getGlobalHttp2MaxActiveStreams()); + Object listenerMaxActiveStreams = endpointConfig.get(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS); + if (listenerMaxActiveStreams == null) { + listenerConfiguration.setHttp2MaxActiveStreams(getGlobalHttp2MaxActiveStreams()); + } else { + long value = ((Long) listenerMaxActiveStreams).longValue(); + listenerConfiguration.setHttp2MaxActiveStreams(value == -1 ? Integer.MAX_VALUE : (int) value); + } double minIdleTimeInStaleState = ((BDecimal) endpointConfig.get(HttpConstants.ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE)).floatValue(); From e5436347f9fba190c50f4d33a5f2e957e071296c Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 11:49:29 +0530 Subject: [PATCH 06/14] Fix Http2Settings API call: maxConcurrentStreams is the Netty method name --- .../listener/http2/Http2SourceConnectionHandlerBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java index c902172339..6777879e32 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/listener/http2/Http2SourceConnectionHandlerBuilder.java @@ -64,7 +64,7 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu this.listenerChannels = listenerChannels; this.initialSettings().maxHeaderListSize(maxHeaderListSize); this.initialSettings().initialWindowSize(initialWindowSize); - this.initialSettings().maxActiveStreams(maxActiveStreams); + this.initialSettings().maxConcurrentStreams(maxActiveStreams); } @Override From c0d4b8645226636486a046a8cf69550e43d6334e Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 20:59:10 +0530 Subject: [PATCH 07/14] Remove unused globalHttp2MaxActiveStreams field and client pool coupling Co-Authored-By: Claude Sonnet 4.6 --- .../io/ballerina/stdlib/http/api/HttpUtil.java | 14 +------------- .../http/api/client/endpoint/InitGlobalPool.java | 2 -- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index 1c1d9edb1a..ed90933646 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -209,8 +209,6 @@ public class HttpUtil { private static final String[] DEFAULT_NAMED_GROUPS = { "X25519MLKEM768", "x25519", "secp256r1", "secp384r1", "secp521r1" }; - private static volatile int globalHttp2MaxActiveStreams = 100; - /** * Set new entity to in/out request/response struct. * @@ -1340,14 +1338,6 @@ public static ConnectionManager getConnectionManager(BMap poolStruct) { return poolManager; } - public static void setGlobalHttp2MaxActiveStreams(int maxActiveStreams) { - globalHttp2MaxActiveStreams = maxActiveStreams; - } - - public static int getGlobalHttp2MaxActiveStreams() { - return globalHttp2MaxActiveStreams; - } - public static void populatePoolingConfig(BMap poolRecord, PoolConfiguration poolConfiguration) { long maxActiveConnections = poolRecord.getIntValue(HttpConstants.CONNECTION_POOLING_MAX_ACTIVE_CONNECTIONS); poolConfiguration.setMaxActivePerPool( @@ -1595,9 +1585,7 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo listenerConfiguration.setHttp2InitialWindowSize(endpointConfig .getIntValue(ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE).intValue()); Object listenerMaxActiveStreams = endpointConfig.get(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS); - if (listenerMaxActiveStreams == null) { - listenerConfiguration.setHttp2MaxActiveStreams(getGlobalHttp2MaxActiveStreams()); - } else { + if (listenerMaxActiveStreams != null) { long value = ((Long) listenerMaxActiveStreams).longValue(); listenerConfiguration.setHttp2MaxActiveStreams(value == -1 ? Integer.MAX_VALUE : (int) value); } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java index e065848089..0ff42a2984 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/InitGlobalPool.java @@ -19,7 +19,6 @@ package io.ballerina.stdlib.http.api.client.endpoint; import io.ballerina.runtime.api.values.BMap; -import io.ballerina.stdlib.http.api.HttpUtil; import io.ballerina.stdlib.http.transport.contractimpl.sender.channel.pool.ConnectionManager; import io.ballerina.stdlib.http.transport.contractimpl.sender.channel.pool.PoolConfiguration; @@ -35,7 +34,6 @@ public class InitGlobalPool { public static void initGlobalPool(BMap globalPoolConfig) { PoolConfiguration globalPool = new PoolConfiguration(); populatePoolingConfig(globalPoolConfig, globalPool); - HttpUtil.setGlobalHttp2MaxActiveStreams(globalPool.getHttp2MaxActiveStreamsPerConnection()); ConnectionManager connectionManager = new ConnectionManager(globalPool); globalPoolConfig.addNativeData(CONNECTION_MANAGER, connectionManager); } From 8500a757842aa6110dfedd161fdea6fc7882cc61 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 21:12:13 +0530 Subject: [PATCH 08/14] Fix README: server stream limit defaults to 100, tunable via http2MaxActiveStreams --- README.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index af6f6613c5..e2e99f7687 100644 --- a/README.md +++ b/README.md @@ -117,15 +117,7 @@ with other widely-used HTTP/2 server implementations such as Nginx and Envoy. When a client reaches this limit on a connection, it will automatically open an additional connection rather than stalling, so normal traffic is unaffected for typical workloads. -The server-side stream limit defaults to the value of the `maxActiveStreamsPerConnection` configurable (default 100), -which also controls how many parallel streams the HTTP client opens per connection. Set it globally in `Config.toml`: - -```toml -[ballerina.http] -maxActiveStreamsPerConnection = 500 -``` - -You can also override it per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: +The server-side stream limit can be tuned per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: ```ballerina listener http:Listener h2Listener = new (9090, { @@ -134,7 +126,7 @@ listener http:Listener h2Listener = new (9090, { ``` Set to `-1` to allow unlimited streams (not recommended for public-facing services). When not set, the listener -inherits the global `maxActiveStreamsPerConnection` value. +defaults to 100. ## Issues and projects From 38f5bce543acc72d2e9b59668dd1b68d52894b49 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 21:18:50 +0530 Subject: [PATCH 09/14] Fix ballerina/README.md: server stream limit tunable via http2MaxActiveStreams --- ballerina/README.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/ballerina/README.md b/ballerina/README.md index 7742ec8483..6aad9c868f 100644 --- a/ballerina/README.md +++ b/ballerina/README.md @@ -108,15 +108,7 @@ with other widely-used HTTP/2 server implementations such as Nginx and Envoy. When a client reaches this limit on a connection, it will automatically open an additional connection rather than stalling, so normal traffic is unaffected for typical workloads. -The server-side stream limit defaults to the value of the `maxActiveStreamsPerConnection` configurable (default 100), -which also controls how many parallel streams the HTTP client opens per connection. Set it globally in `Config.toml`: - -```toml -[ballerina.http] -maxActiveStreamsPerConnection = 500 -``` - -You can also override it per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: +The server-side stream limit can be tuned per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: ```ballerina listener http:Listener h2Listener = new (9090, { @@ -125,4 +117,4 @@ listener http:Listener h2Listener = new (9090, { ``` Set to `-1` to allow unlimited streams (not recommended for public-facing services). When not set, the listener -inherits the global `maxActiveStreamsPerConnection` value. +defaults to 100. From f4b415774eb725dfb668312c54b471eae7e94651 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 21:51:55 +0530 Subject: [PATCH 10/14] Fix method name mismatch: use setHttp2MaxConcurrentStreams in HttpUtil and test --- native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java | 2 +- .../http/transport/http2/Http2MaxConcurrentStreamsTestCase.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index ed90933646..de262c8a21 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -1587,7 +1587,7 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo Object listenerMaxActiveStreams = endpointConfig.get(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS); if (listenerMaxActiveStreams != null) { long value = ((Long) listenerMaxActiveStreams).longValue(); - listenerConfiguration.setHttp2MaxActiveStreams(value == -1 ? Integer.MAX_VALUE : (int) value); + listenerConfiguration.setHttp2MaxConcurrentStreams(value == -1 ? Integer.MAX_VALUE : (int) value); } double minIdleTimeInStaleState = diff --git a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java index 969e2f2807..4e0ca34beb 100644 --- a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java +++ b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/Http2MaxConcurrentStreamsTestCase.java @@ -69,7 +69,7 @@ private void startServer(int port, int maxActiveStreams) throws InterruptedExcep listenerConfiguration.setPort(port); listenerConfiguration.setScheme(Constants.HTTP_SCHEME); listenerConfiguration.setVersion(Constants.HTTP_2_0); - listenerConfiguration.setHttp2MaxActiveStreams(maxActiveStreams); + listenerConfiguration.setHttp2MaxConcurrentStreams(maxActiveStreams); serverConnector = connectorFactory.createServerConnector( TestUtil.getDefaultServerBootstrapConfig(), listenerConfiguration); ServerConnectorFuture future = serverConnector.start(); From 98c70d5273427c76a9ea10e9865f99012d563be1 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 21:53:39 +0530 Subject: [PATCH 11/14] Remove -1 unlimited sentinel from http2MaxActiveStreams listener config --- README.md | 3 +-- ballerina/README.md | 3 +-- ballerina/http_service_endpoint.bal | 6 ++---- .../main/java/io/ballerina/stdlib/http/api/HttpUtil.java | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e2e99f7687..b25f67fc49 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,7 @@ listener http:Listener h2Listener = new (9090, { }); ``` -Set to `-1` to allow unlimited streams (not recommended for public-facing services). When not set, the listener -defaults to 100. +When not set, the listener defaults to 100. ## Issues and projects diff --git a/ballerina/README.md b/ballerina/README.md index 6aad9c868f..db4d152fe2 100644 --- a/ballerina/README.md +++ b/ballerina/README.md @@ -116,5 +116,4 @@ listener http:Listener h2Listener = new (9090, { }); ``` -Set to `-1` to allow unlimited streams (not recommended for public-facing services). When not set, the listener -defaults to 100. +When not set, the listener defaults to 100. diff --git a/ballerina/http_service_endpoint.bal b/ballerina/http_service_endpoint.bal index 7f0f50be59..5d8e009233 100644 --- a/ballerina/http_service_endpoint.bal +++ b/ballerina/http_service_endpoint.bal @@ -148,10 +148,8 @@ public type Local record {| # + gracefulStopTimeout - Grace period of time in seconds for listener gracefulStop # + socketConfig - Provides settings related to server socket configuration # + http2InitialWindowSize - Configuration to change the initial window size in HTTP/2 -# + http2MaxActiveStreams - Maximum concurrent HTTP/2 streams per connection advertised to clients. When set, -# overrides the global `maxActiveStreamsPerConnection` configurable for this listener. -# Set to -1 for unlimited streams. When not set, inherits from the global -# `maxActiveStreamsPerConnection` configurable (itself defaulting to 100) +# + http2MaxActiveStreams - Maximum concurrent HTTP/2 streams per connection advertised to clients. +# When not set, defaults to 100 # + minIdleTimeInStaleState - Minimum time in seconds for a connection to be kept open which has received a GOAWAY. # This only applies for HTTP/2. Default value is 5 minutes. If the value is set to -1, # the connection will be closed after all in-flight streams are completed diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index de262c8a21..e1f4ab5962 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -1587,7 +1587,7 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo Object listenerMaxActiveStreams = endpointConfig.get(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS); if (listenerMaxActiveStreams != null) { long value = ((Long) listenerMaxActiveStreams).longValue(); - listenerConfiguration.setHttp2MaxConcurrentStreams(value == -1 ? Integer.MAX_VALUE : (int) value); + listenerConfiguration.setHttp2MaxConcurrentStreams((int) value); } double minIdleTimeInStaleState = From e71164aef4e90097c3865419785a98b5e733caf5 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 22:02:43 +0530 Subject: [PATCH 12/14] Change http2MaxActiveStreams to int with default 100 in Ballerina, remove Java defaults --- ballerina/http_service_endpoint.bal | 4 ++-- .../main/java/io/ballerina/stdlib/http/api/HttpUtil.java | 7 ++----- .../transport/contract/config/ListenerConfiguration.java | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ballerina/http_service_endpoint.bal b/ballerina/http_service_endpoint.bal index 5d8e009233..3e030f334f 100644 --- a/ballerina/http_service_endpoint.bal +++ b/ballerina/http_service_endpoint.bal @@ -149,7 +149,7 @@ public type Local record {| # + socketConfig - Provides settings related to server socket configuration # + http2InitialWindowSize - Configuration to change the initial window size in HTTP/2 # + http2MaxActiveStreams - Maximum concurrent HTTP/2 streams per connection advertised to clients. -# When not set, defaults to 100 +# Defaults to 100 # + minIdleTimeInStaleState - Minimum time in seconds for a connection to be kept open which has received a GOAWAY. # This only applies for HTTP/2. Default value is 5 minutes. If the value is set to -1, # the connection will be closed after all in-flight streams are completed @@ -166,7 +166,7 @@ public type ListenerConfiguration record {| decimal gracefulStopTimeout = DEFAULT_GRACEFULSTOP_TIMEOUT; ServerSocketConfig socketConfig = {}; int http2InitialWindowSize = 65535; - int? http2MaxActiveStreams = (); + int http2MaxActiveStreams = 100; decimal minIdleTimeInStaleState = 300; decimal timeBetweenStaleEviction = 30; |}; diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index e1f4ab5962..ab76b44494 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -1584,11 +1584,8 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo listenerConfiguration.setPipeliningEnabled(true); //Pipelining is enabled all the time listenerConfiguration.setHttp2InitialWindowSize(endpointConfig .getIntValue(ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE).intValue()); - Object listenerMaxActiveStreams = endpointConfig.get(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS); - if (listenerMaxActiveStreams != null) { - long value = ((Long) listenerMaxActiveStreams).longValue(); - listenerConfiguration.setHttp2MaxConcurrentStreams((int) value); - } + listenerConfiguration.setHttp2MaxConcurrentStreams( + (int) endpointConfig.getIntValue(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS)); double minIdleTimeInStaleState = ((BDecimal) endpointConfig.get(HttpConstants.ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE)).floatValue(); diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java index de4b3e69fc..7a3b457564 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java @@ -64,7 +64,7 @@ public static ListenerConfiguration getDefault() { private boolean socketReuse; private boolean socketKeepAlive; private int http2InitialWindowSize = 65535; - private int http2MaxActiveStreams = 100; + private int http2MaxActiveStreams; private long minIdleTimeInStaleState = 3000000; private long timeBetweenStaleEviction = 30000; From d05659dc80e77bc68f54749d27678ba89b036d6c Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Wed, 1 Jul 2026 22:10:22 +0530 Subject: [PATCH 13/14] Fix Long to int cast and restore 100 default in Java fallback fields --- native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java | 2 +- .../http/transport/contract/config/ListenerConfiguration.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java index ab76b44494..a2a22f06ec 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpUtil.java @@ -1585,7 +1585,7 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo listenerConfiguration.setHttp2InitialWindowSize(endpointConfig .getIntValue(ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE).intValue()); listenerConfiguration.setHttp2MaxConcurrentStreams( - (int) endpointConfig.getIntValue(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS)); + endpointConfig.getIntValue(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS).intValue()); double minIdleTimeInStaleState = ((BDecimal) endpointConfig.get(HttpConstants.ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE)).floatValue(); diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java index 7a3b457564..de4b3e69fc 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contract/config/ListenerConfiguration.java @@ -64,7 +64,7 @@ public static ListenerConfiguration getDefault() { private boolean socketReuse; private boolean socketKeepAlive; private int http2InitialWindowSize = 65535; - private int http2MaxActiveStreams; + private int http2MaxActiveStreams = 100; private long minIdleTimeInStaleState = 3000000; private long timeBetweenStaleEviction = 30000; From 00d615c53d408fe90cc75539241de5dfd734b617 Mon Sep 17 00:00:00 2001 From: Danesh Kuruppu Date: Mon, 6 Jul 2026 10:31:09 +0530 Subject: [PATCH 14/14] Move HTTP/2 stream concurrency docs from README to spec Per review feedback, the detailed HTTP/2 stream concurrency explanation (RFC citation, defaults, tuning example) is a spec-level detail and doesn't belong in the READMEs. Document it properly under Listener in docs/spec/spec.md instead, and fold a brief mention into the existing feature-highlight sentence in both READMEs to keep them consistent with the terse style used for the other Listener/Service features. Co-Authored-By: Claude Sonnet 5 --- README.md | 23 ++--------------------- ballerina/README.md | 23 ++--------------------- docs/spec/spec.md | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index b25f67fc49..8258266bca 100644 --- a/README.md +++ b/README.md @@ -105,27 +105,8 @@ Also, The `listener` can be configured to authenticate and authorize the inbound built-in support for basic authentication, JWT authentication, and OAuth2 authentication. In addition to that, supports both the HTTP/1.1 and HTTP2 protocols and connection keep-alive, content -chunking, HTTP caching, data compression/decompression, payload binding, and authorization can be highlighted as the features of a `Service`. - -#### HTTP/2 Stream Concurrency - -Each HTTP/2 connection can carry multiple requests simultaneously using independent streams. To prevent a single -connection from opening an unbounded number of streams and exhausting server memory, the listener enforces a limit -of **100 concurrent streams per connection** by default. This is the value recommended by RFC 7540 and consistent -with other widely-used HTTP/2 server implementations such as Nginx and Envoy. - -When a client reaches this limit on a connection, it will automatically open an additional connection rather than -stalling, so normal traffic is unaffected for typical workloads. - -The server-side stream limit can be tuned per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: - -```ballerina -listener http:Listener h2Listener = new (9090, { - http2MaxActiveStreams: 500 -}); -``` - -When not set, the listener defaults to 100. +chunking, HTTP caching, data compression/decompression, payload binding, HTTP/2 stream concurrency limiting, and +authorization can be highlighted as the features of a `Service`. ## Issues and projects diff --git a/ballerina/README.md b/ballerina/README.md index db4d152fe2..6bc3977deb 100644 --- a/ballerina/README.md +++ b/ballerina/README.md @@ -96,24 +96,5 @@ Also, The `listener` can be configured to authenticate and authorize the inbound built-in support for basic authentication, JWT authentication, and OAuth2 authentication. In addition to that, supports both the HTTP/1.1 and HTTP2 protocols and connection keep-alive, content -chunking, HTTP caching, data compression/decompression, payload binding, and authorization can be highlighted as the features of a `Service`. - -#### HTTP/2 Stream Concurrency - -Each HTTP/2 connection can carry multiple requests simultaneously using independent streams. To prevent a single -connection from opening an unbounded number of streams and exhausting server memory, the listener enforces a limit -of **100 concurrent streams per connection** by default. This is the value recommended by RFC 7540 and consistent -with other widely-used HTTP/2 server implementations such as Nginx and Envoy. - -When a client reaches this limit on a connection, it will automatically open an additional connection rather than -stalling, so normal traffic is unaffected for typical workloads. - -The server-side stream limit can be tuned per listener using `http2MaxActiveStreams` in `ListenerConfiguration`: - -```ballerina -listener http:Listener h2Listener = new (9090, { - http2MaxActiveStreams: 500 -}); -``` - -When not set, the listener defaults to 100. +chunking, HTTP caching, data compression/decompression, payload binding, HTTP/2 stream concurrency limiting, and +authorization can be highlighted as the features of a `Service`. diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 2857078ad4..5ba5646286 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -24,6 +24,7 @@ The conforming implementation of the specification is released and included in t * 2.1.1. [Automatically starting the service](#211-automatically-starting-the-service) * 2.1.2. [Programmatically starting the service](#212-programmatically-starting-the-service) * 2.1.3. [Default listener](#213-default-listener) + * 2.1.4. [HTTP2 stream concurrency](#214-http2-stream-concurrency) * 2.2. [Service](#22-service) * 2.2.1. [Service type](#221-service-type) * 2.2.2. [Service-base-path](#222-service-base-path) @@ -177,6 +178,7 @@ public type ListenerConfiguration record {| string? server = (); RequestLimitConfigs requestLimits = {}; int http2InitialWindowSize = 65535; + int http2MaxActiveStreams = 100; decimal minIdleTimeInStaleState = 300; decimal timeBetweenStaleEviction = 30; |}; @@ -254,6 +256,21 @@ path = "resources/certs/ballerinaKeystore.p12" password = "ballerina" ``` +#### 2.1.4. HTTP2 stream concurrency + +Each HTTP/2 connection can carry multiple requests concurrently over independent streams. The listener limits the +number of concurrent streams a single connection can open, advertised to clients via the `SETTINGS_MAX_CONCURRENT_STREAMS` +parameter, using the `http2MaxActiveStreams` field of the `ListenerConfiguration`. This defaults to `100`, the value +recommended by [RFC 7540 Section 6.5.2](https://www.rfc-editor.org/rfc/rfc7540#section-6.5.2). + +```ballerina +listener http:Listener h2Listener = new (9090, { + http2MaxActiveStreams: 500 +}); +``` + +A client that reaches this limit on a connection opens an additional connection rather than stalling. + ### 2.2. Service Service is a collection of resources functions, which are the network entry points of a ballerina program. In addition to that a service can contain public and private functions which can be accessed by calling with `self`.