diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
index 00c68113f..adb6e7717 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
@@ -104,7 +104,7 @@
abstract class AbstractH2StreamMultiplexer implements Identifiable, HttpConnection {
- private final H2ControlFrameRateLimiter controlFrameRateLimiter;
+ private final FrameLimitController frameLimitController;
private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024;
@@ -191,7 +191,7 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
final Timeout validateAfterInactivity,
final Timeout pingAckTimeout) {
this(ioSession, frameFactory, idGenerator, httpProcessor, charCodingConfig, h2Config, streamListener,
- validateAfterInactivity, pingAckTimeout, new H2ControlFrameRateLimiter());
+ validateAfterInactivity, pingAckTimeout, new FrameLimitController());
}
AbstractH2StreamMultiplexer(
@@ -204,7 +204,7 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
final H2StreamListener streamListener,
final Timeout validateAfterInactivity,
final Timeout pingAckTimeout,
- final H2ControlFrameRateLimiter controlFrameRateLimiter) {
+ final FrameLimitController frameLimitController) {
this.ioSession = Args.notNull(ioSession, "IO session");
this.frameFactory = Args.notNull(frameFactory, "Frame factory");
this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor");
@@ -233,7 +233,7 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
this.lastActivityTime = System.currentTimeMillis();
this.validateAfterInactivity = validateAfterInactivity;
this.pingAckTimeout = Args.notNull(pingAckTimeout, "PING ACK timeout");
- this.controlFrameRateLimiter = Args.notNull(controlFrameRateLimiter, "Control frame rate limiter");
+ this.frameLimitController = Args.notNull(frameLimitController, "Frame limit controller");
}
@Override
@@ -970,6 +970,12 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
final H2Stream stream = streams.lookupSeen(streamId);
if (stream != null) {
final int errorCode = payload.getInt();
+ // Only newly activated peer-initiated streams count. Valid resets of
+ // long-lived or locally initiated streams remain unaffected.
+ if (streams.isOtherSide(streamId) && !stream.isReserved() && !stream.isClosed()
+ && !frameLimitController.acceptRapidReset(stream.getActivatedAtNanos(), nowNanos)) {
+ throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM, "Excessive rapid HTTP/2 stream resets");
+ }
if (errorCode == H2Error.NO_ERROR.getCode() && allowGracefulAbort(stream)) {
stream.abortGracefully();
requestSessionOutput();
@@ -994,10 +1000,10 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
pingHandler.consumeResponse(ping);
}
} else {
- if (!controlFrameRateLimiter.accept(System.nanoTime())) {
- throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM, "Excessive HTTP/2 PING / SETTINGS frames");
- }
- final ByteBuffer pong = ByteBuffer.allocate(ping.remaining());
+ if (!frameLimitController.acceptControlFrame(System.nanoTime())) {
+ throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM, "Excessive HTTP/2 PING / SETTINGS frames");
+ }
+ final ByteBuffer pong = ByteBuffer.allocate(ping.remaining());
pong.put(ping);
pong.flip();
final RawFrame response = frameFactory.createPingAck(pong);
@@ -1030,7 +1036,7 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR,
"Invalid SETTINGS payload");
}
- if (!controlFrameRateLimiter.accept(System.nanoTime())) {
+ if (!frameLimitController.acceptControlFrame(System.nanoTime())) {
throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM,
"Excessive HTTP/2 PING / SETTINGS frames");
}
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameLimitController.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameLimitController.java
new file mode 100644
index 000000000..d01bd6d9e
--- /dev/null
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameLimitController.java
@@ -0,0 +1,145 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.hc.core5.util.Args;
+
+/**
+ * Per-connection resource policy for unsolicited HTTP/2 control frames. Non-ACK PING and
+ * SETTINGS frames share one token bucket, exactly as before this class existed; rapid
+ * cancellation (RST_STREAM) of peer-initiated streams is charged against its own,
+ * independent bucket.
+ *
+ *
ACK frames, DATA and WINDOW_UPDATE are not charged, as they can represent legitimate
+ * high-throughput or highly concurrent exchanges. A stream reset is only charged against the
+ * RST_STREAM bucket when it arrives shortly after the stream was activated; resets of
+ * long-lived streams are not charged.
+ *
+ * Parameters are hardwired for now; they may become user configurable in the future, at
+ * which point this is the single place that would need a public constructor or setters.
+ *
+ * This is a resource safeguard, not an RFC 9113 protocol-validity rule.
+ */
+final class FrameLimitController {
+
+ static final int CONTROL_FRAME_CAPACITY = 256;
+ static final int CONTROL_FRAME_REFILL_PER_SECOND = 128;
+
+ static final int RST_STREAM_CAPACITY = 128;
+ static final int RST_STREAM_REFILL_PER_SECOND = 64;
+ static final long RST_STREAM_MAX_AGE_NANOS = TimeUnit.SECONDS.toNanos(1);
+
+ private final TokenBucket controlFrameBucket;
+ private final TokenBucket rstStreamBucket;
+ private final long rstStreamMaxAgeNanos;
+
+ FrameLimitController() {
+ this(CONTROL_FRAME_CAPACITY, CONTROL_FRAME_REFILL_PER_SECOND,
+ RST_STREAM_CAPACITY, RST_STREAM_REFILL_PER_SECOND,
+ RST_STREAM_MAX_AGE_NANOS);
+ }
+
+ FrameLimitController(
+ final int controlFrameCapacity,
+ final int controlFrameRefillPerSecond,
+ final int rstStreamCapacity,
+ final int rstStreamRefillPerSecond,
+ final long rstStreamMaxAgeNanos) {
+ this.controlFrameBucket = new TokenBucket(controlFrameCapacity, controlFrameRefillPerSecond);
+ this.rstStreamBucket = new TokenBucket(rstStreamCapacity, rstStreamRefillPerSecond);
+ this.rstStreamMaxAgeNanos = Args.positive(rstStreamMaxAgeNanos, "Maximum reset age");
+ }
+
+ boolean acceptControlFrame(final long nowNanos) {
+ return controlFrameBucket.accept(nowNanos);
+ }
+
+ boolean acceptRapidReset(final long activatedAtNanos, final long nowNanos) {
+ final long age = nowNanos - activatedAtNanos;
+ return age < 0 || age >= rstStreamMaxAgeNanos || rstStreamBucket.accept(nowNanos);
+ }
+
+ /**
+ * Constant-time token bucket, with no locks or per-frame allocation, used to tolerate
+ * bursts and recover over time. Shared by every bucket kept by {@link FrameLimitController};
+ * a new kind of limited frame is just another instance of this class.
+ */
+ static final class TokenBucket {
+
+ private static final long NANOS_PER_SECOND = 1_000_000_000L;
+
+ private final int capacity;
+ private final int refillPerSecond;
+ private final long fullRefillNanos;
+
+ private int tokens;
+ private long previousNanos;
+ private long remainder;
+ private boolean initialized;
+
+ TokenBucket(final int capacity, final int refillPerSecond) {
+ this.capacity = Args.positive(capacity, "Capacity");
+ this.refillPerSecond = Args.positive(refillPerSecond, "Refill per second");
+ this.fullRefillNanos = (NANOS_PER_SECOND * capacity + refillPerSecond - 1) / refillPerSecond;
+ this.tokens = capacity;
+ }
+
+ boolean accept(final long nowNanos) {
+ if (!initialized) {
+ initialized = true;
+ previousNanos = nowNanos;
+ } else {
+ final long elapsed = nowNanos - previousNanos;
+ // Avoid overflow and do not refill on a backward clock jump.
+ if (elapsed > 0) {
+ previousNanos = nowNanos;
+ if (elapsed >= fullRefillNanos) {
+ tokens = capacity;
+ remainder = 0;
+ } else {
+ final long numerator = elapsed * refillPerSecond + remainder;
+ final long refill = numerator / NANOS_PER_SECOND;
+ remainder = numerator % NANOS_PER_SECOND;
+ if (refill > 0) {
+ tokens = (int) Math.min(capacity, (long) tokens + refill);
+ if (tokens == capacity) {
+ remainder = 0;
+ }
+ }
+ }
+ }
+ }
+ if (tokens == 0) {
+ return false;
+ }
+ tokens--;
+ return true;
+ }
+ }
+}
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2ControlFrameRateLimiter.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2ControlFrameRateLimiter.java
deleted file mode 100644
index 77a696ecd..000000000
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2ControlFrameRateLimiter.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation. For more
- * information on the Apache Software Foundation, please see
- * .
- *
- */
-package org.apache.hc.core5.http2.impl.nio;
-
-import org.apache.hc.core5.util.Args;
-
-/**
- * Per-connection resource policy for unsolicited HTTP/2 control frames.
- *
- * The limiter is owned and called by a single HTTP/2 I/O session. Only
- * non-ACK PING and SETTINGS frames are charged. ACK frames, DATA,
- * WINDOW_UPDATE and RST_STREAM are not charged, as they can represent
- * legitimate high-throughput or highly concurrent exchanges.
- *
- * Uses a small token bucket to tolerate bursts and recover over time,
- * with no locks or per-frame allocation. This is a resource safeguard,
- * not an RFC 9113 protocol-validity rule.
- */
-final class H2ControlFrameRateLimiter {
-
- static final int CAPACITY = 256;
- static final int REFILL_PER_SECOND = 128;
- private static final long NANOS_PER_SECOND = 1_000_000_000L;
-
- private final int capacity;
- private final int refillPerSecond;
- private final long fullRefillNanos;
-
- private int tokens;
- private long previousNanos;
- private long remainder;
- private boolean initialized;
-
- H2ControlFrameRateLimiter(final int capacity, final int refillPerSecond) {
- this.capacity = Args.positive(capacity, "Capacity");
- this.refillPerSecond = Args.positive(refillPerSecond, "Refill per second");
- this.fullRefillNanos = (NANOS_PER_SECOND * capacity + refillPerSecond - 1) / refillPerSecond;
- this.tokens = capacity;
- }
-
- H2ControlFrameRateLimiter() {
- this(CAPACITY, REFILL_PER_SECOND);
- }
-
- boolean accept(final long nowNanos) {
- if (!initialized) {
- initialized = true;
- previousNanos = nowNanos;
- } else {
- final long elapsed = nowNanos - previousNanos;
- // Avoid overflow and do not refill on a backward clock jump.
- if (elapsed > 0) {
- previousNanos = nowNanos;
- if (elapsed >= fullRefillNanos) {
- tokens = capacity;
- remainder = 0;
- } else {
- final long numerator = elapsed * refillPerSecond + remainder;
- final long refill = numerator / NANOS_PER_SECOND;
- remainder = numerator % NANOS_PER_SECOND;
- if (refill > 0) {
- tokens = (int) Math.min(capacity, (long) tokens + refill);
- if (tokens == capacity) {
- remainder = 0;
- }
- }
- }
- }
- }
- if (tokens == 0) {
- return false;
- }
- tokens--;
- return true;
- }
-}
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Stream.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Stream.java
index ad080cecc..8aacae579 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Stream.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Stream.java
@@ -63,6 +63,7 @@ class H2Stream implements StreamControl {
private volatile boolean remoteClosed;
private volatile long lastActivityNanos;
+ private volatile long activatedAtNanos;
private volatile Timeout idleTimeout;
private volatile PriorityValue priorityValue;
@@ -112,9 +113,14 @@ private void triggerClosed() {
void activate() {
reserved = false;
touch();
+ activatedAtNanos = lastActivityNanos;
triggerOpen();
}
+ long getActivatedAtNanos() {
+ return activatedAtNanos;
+ }
+
AtomicInteger getOutputWindow() {
return channel.getOutputWindow();
}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java
index 54788d68a..68f38b4f8 100644
--- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java
@@ -167,9 +167,9 @@ public H2StreamMultiplexerImpl(
final Supplier streamHandlerSupplier,
final Timeout validateAfterInactivity,
final Timeout pingAckTimeout,
- final H2ControlFrameRateLimiter controlFrameRateLimiter) {
+ final FrameLimitController frameLimitController) {
super(ioSession, frameFactory, idGenerator, httpProcessor, charCodingConfig, h2Config, streamListener,
- validateAfterInactivity, pingAckTimeout, controlFrameRateLimiter);
+ validateAfterInactivity, pingAckTimeout, frameLimitController);
this.streamHandlerSupplier = streamHandlerSupplier;
}
@@ -2569,8 +2569,11 @@ void testDataOnReservedRemoteStreamIsConnectionProtocolError() throws Exception
void testMalformedSettingsAfterOverheadLimitReportsFrameSizeError() throws Exception {
// A limiter without tokens that cannot refill: the multiplexer takes its timestamps
// from System#nanoTime(), which is always earlier than the one used here.
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(1, 1);
- Assertions.assertTrue(limiter.accept(Long.MAX_VALUE));
+ final FrameLimitController limiter = new FrameLimitController(
+ 1, 1,
+ FrameLimitController.RST_STREAM_CAPACITY, FrameLimitController.RST_STREAM_REFILL_PER_SECOND,
+ FrameLimitController.RST_STREAM_MAX_AGE_NANOS);
+ Assertions.assertTrue(limiter.acceptControlFrame(Long.MAX_VALUE));
final AbstractH2StreamMultiplexer mux = new H2StreamMultiplexerImpl(
protocolIOSession,
@@ -2601,4 +2604,57 @@ void testMalformedSettingsAfterOverheadLimitReportsFrameSizeError() throws Excep
Assertions.assertEquals(H2Error.ENHANCE_YOUR_CALM,
H2Error.getByCode(overheadError.getCode()));
}
+ @Test
+ void testRapidResetRejectsExcessivePeerCancellations() throws Exception {
+ final H2Config config = H2Config.custom().build();
+ final AbstractH2StreamMultiplexer mux = new H2StreamMultiplexerImpl(
+ protocolIOSession, FRAME_FACTORY, StreamIdGenerator.EVEN, httpProcessor,
+ CharCodingConfig.DEFAULT, config, h2StreamListener, () -> streamHandler,
+ null, Timeout.ofSeconds(5),
+ new FrameLimitController(
+ FrameLimitController.CONTROL_FRAME_CAPACITY, FrameLimitController.CONTROL_FRAME_REFILL_PER_SECOND,
+ 1, 1, TimeUnit.DAYS.toNanos(1)));
+
+ final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(),
+ CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT));
+ final ByteArrayBuffer headerBlock = new ByteArrayBuffer(128);
+ encoder.encodeHeaders(headerBlock, Arrays.asList(
+ new BasicHeader(":method", "GET"),
+ new BasicHeader(":scheme", "http"),
+ new BasicHeader(":path", "/"),
+ new BasicHeader(":authority", "localhost")), config.isCompressionEnabled());
+
+ for (final int streamId : new int[] {1, 3}) {
+ final RawFrame headers = FRAME_FACTORY.createHeaders(streamId,
+ ByteBuffer.wrap(headerBlock.array(), 0, headerBlock.length()), true, true);
+ mux.onInput(ByteBuffer.wrap(encodeFrame(headers)));
+ final RawFrame reset = FRAME_FACTORY.createResetStream(streamId, H2Error.CANCEL);
+ if (streamId == 1) {
+ Assertions.assertDoesNotThrow(() -> mux.onInput(ByteBuffer.wrap(encodeFrame(reset))));
+ } else {
+ final H2ConnectionException ex = Assertions.assertThrows(H2ConnectionException.class,
+ () -> mux.onInput(ByteBuffer.wrap(encodeFrame(reset))));
+ Assertions.assertEquals(H2Error.ENHANCE_YOUR_CALM, H2Error.getByCode(ex.getCode()));
+ }
+ }
+ }
+
+ @Test
+ void testResetOfLocallyInitiatedStreamsDoesNotExhaustRemoteLimit() throws Exception {
+ final AbstractH2StreamMultiplexer mux = new H2StreamMultiplexerImpl(
+ protocolIOSession, FRAME_FACTORY, StreamIdGenerator.ODD, httpProcessor,
+ CharCodingConfig.DEFAULT, H2Config.custom().build(), h2StreamListener,
+ () -> streamHandler, null, Timeout.ofSeconds(5),
+ new FrameLimitController(
+ FrameLimitController.CONTROL_FRAME_CAPACITY, FrameLimitController.CONTROL_FRAME_REFILL_PER_SECOND,
+ 1, 1, TimeUnit.DAYS.toNanos(1)));
+
+ for (final int streamId : new int[] {1, 3}) {
+ mux.createStream(mux.createChannel(streamId), streamHandler);
+ final RawFrame reset = FRAME_FACTORY.createResetStream(streamId, H2Error.CANCEL);
+ Assertions.assertDoesNotThrow(() -> mux.onInput(ByteBuffer.wrap(encodeFrame(reset))));
+ }
+ }
+
+
}
\ No newline at end of file
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestFrameLimitController.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestFrameLimitController.java
new file mode 100644
index 000000000..f70c0676c
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestFrameLimitController.java
@@ -0,0 +1,222 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestFrameLimitController {
+
+ private static final long SECOND = TimeUnit.SECONDS.toNanos(1);
+
+ // A generous bucket for the frame kind a given test is not exercising, so it never
+ // interferes with the one under test.
+ private static final int GENEROUS_CAPACITY = 1_000_000;
+ private static final int GENEROUS_REFILL = 1_000_000;
+
+ private static FrameLimitController withControlFrame(final int capacity, final int refillPerSecond) {
+ return new FrameLimitController(capacity, refillPerSecond, GENEROUS_CAPACITY, GENEROUS_REFILL, SECOND);
+ }
+
+ private static FrameLimitController withRapidReset(final int capacity, final int refillPerSecond, final long maxAgeNanos) {
+ return new FrameLimitController(GENEROUS_CAPACITY, GENEROUS_REFILL, capacity, refillPerSecond, maxAgeNanos);
+ }
+
+ // -------------------------------------------------------------------------
+ // Control frame bucket (PING and SETTINGS share this one, exactly as before
+ // this class existed: ported from the former TestH2ControlFrameRateLimiter)
+ // -------------------------------------------------------------------------
+
+ private static void assertAccepted(final FrameLimitController controller, final int count, final long nowNanos) {
+ for (int i = 0; i < count; i++) {
+ Assertions.assertTrue(controller.acceptControlFrame(nowNanos), "frame " + (i + 1) + " of " + count);
+ }
+ }
+
+ @Test
+ void testInitialCapacity() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 4, 0L);
+ }
+
+ @Test
+ void testExhaustion() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 4, 0L);
+ Assertions.assertFalse(controller.acceptControlFrame(0L));
+ }
+
+ @Test
+ void testProportionalRefill() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 4, 0L);
+
+ // 2 frames per second: one second brings back 2 frames, not 4
+ assertAccepted(controller, 2, SECOND);
+ Assertions.assertFalse(controller.acceptControlFrame(SECOND));
+
+ assertAccepted(controller, 2, 2 * SECOND);
+ Assertions.assertFalse(controller.acceptControlFrame(2 * SECOND));
+ }
+
+ @Test
+ void testFractionalRefill() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 4, 0L);
+
+ // 0.25 s at 2 frames per second is half a frame
+ Assertions.assertFalse(controller.acceptControlFrame(SECOND / 4));
+ // another 0.25 s completes the frame
+ Assertions.assertTrue(controller.acceptControlFrame(SECOND / 2));
+ Assertions.assertFalse(controller.acceptControlFrame(SECOND / 2));
+ }
+
+ @Test
+ void testRecoveryAfterRejection() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 4, 0L);
+ Assertions.assertFalse(controller.acceptControlFrame(0L));
+
+ Assertions.assertTrue(controller.acceptControlFrame(SECOND));
+ }
+
+ @Test
+ void testRefillDoesNotExceedCapacity() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 1, 0L);
+
+ // 3 frames left, one second adds 2: 5 would exceed the capacity of 4
+ assertAccepted(controller, 4, SECOND);
+ Assertions.assertFalse(controller.acceptControlFrame(SECOND));
+ }
+
+ @Test
+ void testIdleTimeRestoresCapacityOnly() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 4, 0L);
+
+ assertAccepted(controller, 4, 10 * SECOND);
+ Assertions.assertFalse(controller.acceptControlFrame(10 * SECOND));
+ }
+
+ @Test
+ void testCustomParameters() {
+ final FrameLimitController controller = withControlFrame(10, 5);
+
+ assertAccepted(controller, 10, 0L);
+ Assertions.assertFalse(controller.acceptControlFrame(0L));
+
+ assertAccepted(controller, 5, SECOND);
+ Assertions.assertFalse(controller.acceptControlFrame(SECOND));
+ }
+
+ @Test
+ void testDefaultParameters() {
+ final FrameLimitController controller = new FrameLimitController();
+
+ assertAccepted(controller, FrameLimitController.CONTROL_FRAME_CAPACITY, 0L);
+ Assertions.assertFalse(controller.acceptControlFrame(0L));
+
+ assertAccepted(controller, FrameLimitController.CONTROL_FRAME_REFILL_PER_SECOND, SECOND);
+ Assertions.assertFalse(controller.acceptControlFrame(SECOND));
+ }
+
+ @Test
+ void testClockRollbackDoesNotRefill() {
+ final FrameLimitController controller = withControlFrame(4, 2);
+ assertAccepted(controller, 4, 100L);
+ Assertions.assertFalse(controller.acceptControlFrame(99L));
+ }
+
+ @Test
+ void testInvalidParameters() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> withControlFrame(0, 1));
+ Assertions.assertThrows(IllegalArgumentException.class, () -> withControlFrame(-1, 1));
+ Assertions.assertThrows(IllegalArgumentException.class, () -> withControlFrame(1, 0));
+ Assertions.assertThrows(IllegalArgumentException.class, () -> withControlFrame(1, -1));
+ Assertions.assertThrows(IllegalArgumentException.class, () -> withRapidReset(1, 1, 0L));
+ Assertions.assertThrows(IllegalArgumentException.class, () -> withRapidReset(1, 1, -1L));
+ }
+
+ // -------------------------------------------------------------------------
+ // Rapid reset has its own, independent bucket
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testRapidResetHasItsOwnBudgetIndependentOfControlFrames() {
+ final FrameLimitController controller = new FrameLimitController(1, 1, 1, 1, SECOND);
+
+ Assertions.assertTrue(controller.acceptRapidReset(0L, 0L));
+ Assertions.assertFalse(controller.acceptRapidReset(0L, 0L));
+ // Exhausting RST_STREAM must not affect PING / SETTINGS.
+ Assertions.assertTrue(controller.acceptControlFrame(0L));
+ }
+
+ @Test
+ void testShortLivedResetsAreLimited() {
+ final FrameLimitController controller = withRapidReset(2, 1, SECOND);
+ Assertions.assertTrue(controller.acceptRapidReset(0, 0));
+ Assertions.assertTrue(controller.acceptRapidReset(0, 0));
+ Assertions.assertFalse(controller.acceptRapidReset(0, 0));
+ }
+
+ @Test
+ void testLongLivedStreamResetIsNotCharged() {
+ final FrameLimitController controller = withRapidReset(1, 1, SECOND);
+ Assertions.assertTrue(controller.acceptRapidReset(0, 0));
+ Assertions.assertFalse(controller.acceptRapidReset(0, 0));
+ Assertions.assertTrue(controller.acceptRapidReset(0, SECOND));
+ Assertions.assertFalse(controller.acceptRapidReset(0, 0));
+ }
+
+ @Test
+ void testRapidResetTokensRecoverOverTime() {
+ final FrameLimitController controller = withRapidReset(2, 1, SECOND);
+ Assertions.assertTrue(controller.acceptRapidReset(0, 0));
+ Assertions.assertTrue(controller.acceptRapidReset(0, 0));
+ Assertions.assertFalse(controller.acceptRapidReset(0, 0));
+ Assertions.assertTrue(controller.acceptRapidReset(SECOND, SECOND));
+ Assertions.assertFalse(controller.acceptRapidReset(SECOND, SECOND));
+ }
+
+ @Test
+ void testIndependentConnectionsHaveIndependentBudgets() {
+ final FrameLimitController first = withRapidReset(1, 1, SECOND);
+ final FrameLimitController second = withRapidReset(1, 1, SECOND);
+ Assertions.assertTrue(first.acceptRapidReset(0, 0));
+ Assertions.assertFalse(first.acceptRapidReset(0, 0));
+ Assertions.assertTrue(second.acceptRapidReset(0, 0));
+ }
+
+ @Test
+ void testClockMovingBackwardsDoesNotRejectRapidReset() {
+ final FrameLimitController controller = withRapidReset(1, 1, SECOND);
+ Assertions.assertTrue(controller.acceptRapidReset(100, 99));
+ }
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestH2ControlFrameRateLimiter.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestH2ControlFrameRateLimiter.java
deleted file mode 100644
index 26a9256df..000000000
--- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestH2ControlFrameRateLimiter.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation. For more
- * information on the Apache Software Foundation, please see
- * .
- *
- */
-package org.apache.hc.core5.http2.impl.nio;
-
-import java.util.concurrent.TimeUnit;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-class TestH2ControlFrameRateLimiter {
-
- private static final long SECOND = TimeUnit.SECONDS.toNanos(1);
-
- private static void assertAccepted(final H2ControlFrameRateLimiter limiter, final int count, final long nowNanos) {
- for (int i = 0; i < count; i++) {
- Assertions.assertTrue(limiter.accept(nowNanos), "frame " + (i + 1) + " of " + count);
- }
- }
-
- @Test
- void testInitialCapacity() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
-
- assertAccepted(limiter, 4, 0L);
- }
-
- @Test
- void testExhaustion() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
-
- assertAccepted(limiter, 4, 0L);
- Assertions.assertFalse(limiter.accept(0L));
- }
-
- @Test
- void testProportionalRefill() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
- assertAccepted(limiter, 4, 0L);
-
- // 2 frames per second: one second brings back 2 frames, not 4
- assertAccepted(limiter, 2, SECOND);
- Assertions.assertFalse(limiter.accept(SECOND));
-
- assertAccepted(limiter, 2, 2 * SECOND);
- Assertions.assertFalse(limiter.accept(2 * SECOND));
- }
-
- @Test
- void testFractionalRefill() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
- assertAccepted(limiter, 4, 0L);
-
- // 0.25 s at 2 frames per second is half a frame
- Assertions.assertFalse(limiter.accept(SECOND / 4));
- // another 0.25 s completes the frame
- Assertions.assertTrue(limiter.accept(SECOND / 2));
- Assertions.assertFalse(limiter.accept(SECOND / 2));
- }
-
- @Test
- void testRecoveryAfterRejection() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
- assertAccepted(limiter, 4, 0L);
- Assertions.assertFalse(limiter.accept(0L));
-
- Assertions.assertTrue(limiter.accept(SECOND));
- }
-
- @Test
- void testRefillDoesNotExceedCapacity() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
- assertAccepted(limiter, 1, 0L);
-
- // 3 frames left, one second adds 2: 5 would exceed the capacity of 4
- assertAccepted(limiter, 4, SECOND);
- Assertions.assertFalse(limiter.accept(SECOND));
- }
-
- @Test
- void testIdleTimeRestoresCapacityOnly() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
- assertAccepted(limiter, 4, 0L);
-
- assertAccepted(limiter, 4, 10 * SECOND);
- Assertions.assertFalse(limiter.accept(10 * SECOND));
- }
-
- @Test
- void testCustomParameters() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(10, 5);
-
- assertAccepted(limiter, 10, 0L);
- Assertions.assertFalse(limiter.accept(0L));
-
- assertAccepted(limiter, 5, SECOND);
- Assertions.assertFalse(limiter.accept(SECOND));
- }
-
- @Test
- void testDefaultParameters() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter();
-
- assertAccepted(limiter, H2ControlFrameRateLimiter.CAPACITY, 0L);
- Assertions.assertFalse(limiter.accept(0L));
-
- assertAccepted(limiter, H2ControlFrameRateLimiter.REFILL_PER_SECOND, SECOND);
- Assertions.assertFalse(limiter.accept(SECOND));
- }
-
- @Test
- void testClockRollbackDoesNotRefill() {
- final H2ControlFrameRateLimiter limiter = new H2ControlFrameRateLimiter(4, 2);
- assertAccepted(limiter, 4, 100L);
-
- Assertions.assertFalse(limiter.accept(99L));
- }
-
- @Test
- void testInvalidParameters() {
- Assertions.assertThrows(IllegalArgumentException.class, () -> new H2ControlFrameRateLimiter(0, 1));
- Assertions.assertThrows(IllegalArgumentException.class, () -> new H2ControlFrameRateLimiter(-1, 1));
- Assertions.assertThrows(IllegalArgumentException.class, () -> new H2ControlFrameRateLimiter(1, 0));
- Assertions.assertThrows(IllegalArgumentException.class, () -> new H2ControlFrameRateLimiter(1, -1));
- }
-
-}