Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
bc9ff6b
Fix CVE-2026-47244: enforce HTTP/2 maxConcurrentStreams limit on server
daneshk Jun 30, 2026
fcb295d
Document HTTP/2 maxConcurrentStreams limit and system property override
daneshk Jun 30, 2026
06a6e2b
Fix test to use ListenerConfiguration instead of system property for …
daneshk Jun 30, 2026
d2bf76b
Rework CVE-2026-47244 fix to derive maxActiveStreams from global pool…
daneshk Jul 1, 2026
d4c1995
Add optional http2MaxActiveStreams field to ListenerConfiguration
daneshk Jul 1, 2026
e543634
Fix Http2Settings API call: maxConcurrentStreams is the Netty method …
daneshk Jul 1, 2026
c0d4b86
Remove unused globalHttp2MaxActiveStreams field and client pool coupling
daneshk Jul 1, 2026
8500a75
Fix README: server stream limit defaults to 100, tunable via http2Max…
daneshk Jul 1, 2026
38f5bce
Fix ballerina/README.md: server stream limit tunable via http2MaxActi…
daneshk Jul 1, 2026
f4b4157
Fix method name mismatch: use setHttp2MaxConcurrentStreams in HttpUti…
daneshk Jul 1, 2026
98c70d5
Remove -1 unlimited sentinel from http2MaxActiveStreams listener config
daneshk Jul 1, 2026
e71164a
Change http2MaxActiveStreams to int with default 100 in Ballerina, re…
daneshk Jul 1, 2026
d05659d
Fix Long to int cast and restore 100 default in Java fallback fields
daneshk Jul 1, 2026
00d615c
Move HTTP/2 stream concurrency docs from README to spec
daneshk Jul 6, 2026
9f994af
Merge branch 'master' into fix/http2-max-concurrent-streams
daneshk Jul 12, 2026
7acd87a
Apply suggestions from code review
daneshk Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +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`.
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

Expand Down
3 changes: 2 additions & 1 deletion ballerina/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,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`.
chunking, HTTP caching, data compression/decompression, payload binding, HTTP/2 stream concurrency limiting, and
authorization can be highlighted as the features of a `Service`.
3 changes: 3 additions & 0 deletions ballerina/http_service_endpoint.bal
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +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.
# 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
Expand All @@ -164,6 +166,7 @@ public type ListenerConfiguration record {|
decimal gracefulStopTimeout = DEFAULT_GRACEFULSTOP_TIMEOUT;
ServerSocketConfig socketConfig = {};
int http2InitialWindowSize = 65535;
int http2MaxActiveStreams = 100;
decimal minIdleTimeInStaleState = 300;
decimal timeBetweenStaleEviction = 30;
|};
Expand Down
17 changes: 17 additions & 0 deletions docs/spec/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. [HTTP/2 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)
Expand Down Expand Up @@ -177,6 +178,7 @@ public type ListenerConfiguration record {|
string? server = ();
RequestLimitConfigs requestLimits = {};
int http2InitialWindowSize = 65535;
int http2MaxActiveStreams = 100;
decimal minIdleTimeInStaleState = 300;
decimal timeBetweenStaleEviction = 30;
|};
Expand Down Expand Up @@ -254,6 +256,21 @@ path = "resources/certs/ballerinaKeystore.p12"
password = "ballerina"
```

#### 2.1.4. HTTP/2 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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1605,6 +1606,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(
Comment thread
daneshk marked this conversation as resolved.
endpointConfig.getIntValue(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS).intValue());

double minIdleTimeInStaleState =
((BDecimal) endpointConfig.get(HttpConstants.ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE)).floatValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public static ListenerConfiguration getDefault() {
private boolean socketReuse;
private boolean socketKeepAlive;
private int http2InitialWindowSize = 65535;
private int http2MaxActiveStreams = 100;
private long minIdleTimeInStaleState = 3000000;
private long timeBetweenStaleEviction = 30000;

Expand Down Expand Up @@ -285,6 +286,14 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) {
this.http2InitialWindowSize = http2InitialWindowSize;
}

public int getHttp2MaxConcurrentStreams() {
return http2MaxActiveStreams;
}

public void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) {
this.http2MaxActiveStreams = http2MaxActiveStreams;
}

public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) {
this.timeBetweenStaleEviction = timeBetweenStaleEviction;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public class HttpServerChannelInitializer extends ChannelInitializer<SocketChann
private EventExecutorGroup pipeliningGroup;
private boolean webSocketCompressionEnabled;
private int http2InitialWindowSize;
private int http2MaxActiveStreams = 100;
private long minIdleTimeInStaleState;
private long timeBetweenStaleEviction;
private final BlockingQueue<Http2SourceHandler> http2StaleSourceHandlers = new LinkedBlockingQueue<>();
Expand Down Expand Up @@ -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, http2MaxActiveStreams));
// Add http2 upgrade decoder and upgrade handler
final HttpServerCodec sourceCodec = new HttpServerCodec(reqSizeValidationConfig.getMaxInitialLineLength(),
reqSizeValidationConfig.getMaxHeaderSize(),
Expand All @@ -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.http2MaxActiveStreams).build());
} else {
return null;
}
Expand Down Expand Up @@ -355,6 +356,10 @@ void setHttp2InitialWindowSize(int http2InitialWindowSize) {
this.http2InitialWindowSize = http2InitialWindowSize;
}

void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) {
this.http2MaxActiveStreams = http2MaxActiveStreams;
}

void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) {
this.timeBetweenStaleEviction = timeBetweenStaleEviction;
}
Expand Down Expand Up @@ -447,7 +452,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
new Http2SourceConnectionHandlerBuilder(
interfaceId, serverConnectorFuture, serverName, channelInitializer,
allChannels, listenerChannels, reqSizeValidationConfig.getMaxHeaderSize(),
http2InitialWindowSize).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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) {
httpServerChannelInitializer.setHttp2InitialWindowSize(http2InitialWindowSize);
}

public void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) {
httpServerChannelInitializer.setHttp2MaxConcurrentStreams(http2MaxActiveStreams);
}

public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) {
httpServerChannelInitializer.setTimeBetweenStaleEviction(timeBetweenStaleEviction);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu
ChannelGroup allChannels,
ChannelGroup listenerChannels,
long maxHeaderListSize,
int initialWindowSize) {
int initialWindowSize,
int maxActiveStreams) {
this.interfaceId = interfaceId;
this.serverConnectorFuture = serverConnectorFuture;
this.serverName = serverName;
Expand All @@ -63,6 +64,7 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu
this.listenerChannels = listenerChannels;
this.initialSettings().maxHeaderListSize(maxHeaderListSize);
this.initialSettings().initialWindowSize(initialWindowSize);
this.initialSettings().maxConcurrentStreams(maxActiveStreams);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ public class Http2WithPriorKnowledgeHandler extends ChannelInboundHandlerAdapter
private ChannelGroup listenerChannels;
private long maxHeaderListSize;
private int initialWindowSize;
private int maxActiveStreams;

public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName,
ServerConnectorFuture serverConnectorFuture,
HttpServerChannelInitializer serverChannelInitializer,
ChannelGroup allChannels,
ChannelGroup listenerChannels,
long maxHeaderListSize,
int initialWindowSize) {
int initialWindowSize,
int maxActiveStreams) {
this.interfaceId = interfaceId;
this.serverName = serverName;
this.serverConnectorFuture = serverConnectorFuture;
Expand All @@ -65,6 +67,7 @@ public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName,
this.listenerChannels = listenerChannels;
this.maxHeaderListSize = maxHeaderListSize;
this.initialWindowSize = initialWindowSize;
this.maxActiveStreams = maxActiveStreams;
}

@Override
Expand All @@ -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,
maxActiveStreams).build());
safelyRemoveHandlers(pipeline, Constants.HTTP2_UPGRADE_HANDLER,
Constants.HTTP_COMPRESSOR, Constants.HTTP_TRACE_LOG_HANDLER);
}
Expand Down
Loading
Loading