--- netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java	2025-08-22 12:22:10.602600040 +0200
+++ netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java	2025-08-22 12:31:29.844947427 +0200
@@ -109,6 +109,10 @@
     private boolean autoAckPingFrame = true;
     private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;
     private int maxConsecutiveEmptyFrames = 2;
+    private Integer maxDecodedRstFramesPerWindow;
+    private int maxDecodedRstFramesSecondsPerWindow = 30;
+    private Integer maxEncodedRstFramesPerWindow;
+    private int maxEncodedRstFramesSecondsPerWindow = 30;
 
     /**
      * Sets the {@link Http2Settings} to use for the initial connection settings exchange.
@@ -420,6 +424,36 @@
     }
 
     /**
+     * Sets the maximum number RST frames that are allowed per window before
+     * the connection is closed. This allows to protect against the remote peer flooding us with such frames and
+     * so use up a lot of CPU.
+     *
+     * {@code 0} for any of the parameters means no protection should be applied.
+     */
+    protected B decoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int secondsPerWindow) {
+        enforceNonCodecConstraints("decoderEnforceMaxRstFramesPerWindow");
+        this.maxDecodedRstFramesPerWindow = checkPositiveOrZero(
+                maxRstFramesPerWindow, "maxRstFramesPerWindow");
+        this.maxDecodedRstFramesSecondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
+        return self();
+    }
+
+    /**
+     * Sets the maximum number RST frames that are allowed per window before
+     * the connection is closed. This allows to protect against the remote peer that will trigger us to generate a flood
+     * of RST frames and so use up a lot of CPU.
+     *
+     * {@code 0} for any of the parameters means no protection should be applied.
+     */
+    protected B encoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int secondsPerWindow) {
+        enforceNonCodecConstraints("encoderEnforceMaxRstFramesPerWindow");
+        this.maxEncodedRstFramesPerWindow = checkPositiveOrZero(
+                maxRstFramesPerWindow, "maxRstFramesPerWindow");
+        this.maxEncodedRstFramesSecondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
+        return self();
+    }
+
+    /**
      * Sets the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before
      * the connection is closed. This allows to protected against the remote peer flooding us with such frames and
      * so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag.
@@ -524,6 +558,21 @@
         if (maxQueuedControlFrames != 0) {
             encoder = new Http2ControlFrameLimitEncoder(encoder, maxQueuedControlFrames);
         }
+        final int maxEncodedRstFrames;
+        if (maxEncodedRstFramesPerWindow == null) {
+            // Only enable by default on the server.
+            if (isServer()) {
+                maxEncodedRstFrames = DEFAULT_MAX_RST_FRAMES_PER_CONNECTION_FOR_SERVER;
+            } else {
+                maxEncodedRstFrames = 0;
+            }
+        } else {
+            maxEncodedRstFrames = maxEncodedRstFramesPerWindow;
+        }
+        if (maxEncodedRstFrames > 0 && maxEncodedRstFramesSecondsPerWindow > 0) {
+            encoder = new Http2MaxRstFrameLimitEncoder(
+                    encoder, maxEncodedRstFrames, maxEncodedRstFramesSecondsPerWindow);
+        }
         if (encoderEnforceMaxConcurrentStreams) {
             if (connection.isServer()) {
                 encoder.close();
@@ -545,6 +594,20 @@
         if (maxConsecutiveEmptyDataFrames > 0) {
             decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
         }
+        final int maxDecodedRstFrames;
+        if (maxDecodedRstFramesPerWindow == null) {
+            // Only enable by default on the server.
+            if (isServer()) {
+                maxDecodedRstFrames = DEFAULT_MAX_RST_FRAMES_PER_CONNECTION_FOR_SERVER;
+            } else {
+                maxDecodedRstFrames = 0;
+            }
+        } else {
+            maxDecodedRstFrames = maxDecodedRstFramesPerWindow;
+        }
+        if (maxDecodedRstFrames > 0 && maxDecodedRstFramesSecondsPerWindow > 0) {
+            decoder = new Http2MaxRstFrameDecoder(decoder, maxDecodedRstFrames, maxDecodedRstFramesSecondsPerWindow);
+        }
         final T handler;
         try {
             // Call the abstract build method
--- netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java	2025-08-22 12:22:10.604092284 +0200
+++ netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java	2025-08-22 12:22:20.988466045 +0200
@@ -19,6 +19,8 @@
 import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
 import io.netty.util.internal.UnstableApi;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 /**
  * Builder which builds {@link Http2ConnectionHandler} objects.
  */
@@ -99,6 +101,12 @@
     }
 
     @Override
+    public Http2ConnectionHandlerBuilder encoderEnforceMaxRstFramesPerWindow(
+            int maxRstFramesPerWindow, int secondsPerWindow) {
+        return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
+    }
+
+    @Override
     public Http2ConnectionHandler build() {
         return super.build();
     }
--- netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java	2025-08-22 12:22:10.604515201 +0200
+++ netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java	2025-08-22 12:22:20.988631605 +0200
@@ -177,6 +177,12 @@
         return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
     }
 
+    @Override
+    public Http2FrameCodecBuilder encoderEnforceMaxRstFramesPerWindow(
+            int maxRstFramesPerWindow, int secondsPerWindow) {
+        return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
+    }
+
     /**
      * Build a {@link Http2FrameCodec} object.
      */
--- netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameLimitEncoder.java	1970-01-01 01:00:00.000000000 +0100
+++ netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameLimitEncoder.java	2025-08-22 12:22:20.988852506 +0200
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2025 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ * https://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.netty.handler.codec.http2;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * {@link DecoratingHttp2ConnectionEncoder} which guards against a remote peer that will trigger a massive amount
+ * of RST frames on an existing connection.
+ * This encoder will tear-down the connection once we reached the configured limit to reduce the risk of DDOS.
+ */
+final class Http2MaxRstFrameLimitEncoder extends DecoratingHttp2ConnectionEncoder {
+    private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2MaxRstFrameLimitEncoder.class);
+
+    private final long nanosPerWindow;
+    private final int maxRstFramesPerWindow;
+    private long lastRstFrameNano = System.nanoTime();
+    private int sendRstInWindow;
+    private Http2LifecycleManager lifecycleManager;
+
+    Http2MaxRstFrameLimitEncoder(Http2ConnectionEncoder delegate, int maxRstFramesPerWindow, int secondsPerWindow) {
+        super(delegate);
+        this.maxRstFramesPerWindow = maxRstFramesPerWindow;
+        this.nanosPerWindow = TimeUnit.SECONDS.toNanos(secondsPerWindow);
+    }
+
+    @Override
+    public void lifecycleManager(Http2LifecycleManager lifecycleManager) {
+        this.lifecycleManager = lifecycleManager;
+        super.lifecycleManager(lifecycleManager);
+    }
+
+    @Override
+    public ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
+                                        ChannelPromise promise) {
+        ChannelFuture future = super.writeRstStream(ctx, streamId, errorCode, promise);
+        if (countRstFrameErrorCode(errorCode)) {
+            long currentNano = System.nanoTime();
+            if (currentNano - lastRstFrameNano >= nanosPerWindow) {
+                lastRstFrameNano = currentNano;
+                sendRstInWindow = 1;
+            } else {
+                sendRstInWindow++;
+                if (sendRstInWindow > maxRstFramesPerWindow) {
+                    Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
+                            "Maximum number %d of RST frames frames reached within %d seconds", maxRstFramesPerWindow,
+                            TimeUnit.NANOSECONDS.toSeconds(nanosPerWindow));
+
+                    logger.debug("{} Maximum number {} of RST frames reached within {} seconds, " +
+                                    "closing connection with {} error", ctx.channel(), maxRstFramesPerWindow,
+                            TimeUnit.NANOSECONDS.toSeconds(nanosPerWindow), exception.error(),
+                            exception);
+                    // First notify the Http2LifecycleManager and then close the connection.
+                    lifecycleManager.onError(ctx, true, exception);
+                    ctx.close();
+                }
+            }
+        }
+
+        return future;
+    }
+
+    private boolean countRstFrameErrorCode(long errorCode) {
+        // Don't count CANCEL and NO_ERROR as these might be ok.
+        return errorCode != Http2Error.CANCEL.code() && errorCode != Http2Error.NO_ERROR.code();
+    }
+}
--- netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java	2025-08-22 12:22:10.605095097 +0200
+++ netty-netty-4.1.44.Final/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java	2025-08-22 12:22:20.989048124 +0200
@@ -207,6 +207,12 @@
     }
 
     @Override
+    public Http2MultiplexCodecBuilder encoderEnforceMaxRstFramesPerWindow(
+            int maxRstFramesPerWindow, int secondsPerWindow) {
+        return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
+    }
+
+    @Override
     public Http2MultiplexCodec build() {
         Http2FrameWriter frameWriter = this.frameWriter;
         if (frameWriter != null) {
--- netty-netty-4.1.44.Final/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameLimitEncoderTest.java	1970-01-01 01:00:00.000000000 +0100
+++ netty-netty-4.1.44.Final/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameLimitEncoderTest.java	2025-08-22 12:22:20.989416924 +0200
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2019 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ * https://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.netty.handler.codec.http2;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelConfig;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelMetadata;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultChannelPromise;
+import io.netty.channel.DefaultMessageSizeEstimator;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_FRAME_SIZE;
+import static io.netty.handler.codec.http2.Http2Error.CANCEL;
+import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM;
+import static io.netty.handler.codec.http2.Http2Error.NO_ERROR;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link Http2MaxRstFrameLimitEncoder}.
+ */
+public class Http2MaxRstFrameLimitEncoderTest {
+
+    private Http2MaxRstFrameLimitEncoder encoder;
+
+    @Mock
+    private Http2FrameWriter writer;
+
+    @Mock
+    private ChannelHandlerContext ctx;
+
+    @Mock
+    private Channel channel;
+
+    @Mock
+    private Channel.Unsafe unsafe;
+
+    @Mock
+    private ChannelConfig config;
+
+    @Mock
+    private EventExecutor executor;
+
+    private final Queue<ChannelPromise> goAwayPromises = new ArrayDeque<ChannelPromise>();
+
+    /**
+     * Init fields and do mocking.
+     */
+    @BeforeEach
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        Http2FrameWriter.Configuration configuration = mock(Http2FrameWriter.Configuration.class);
+        Http2FrameSizePolicy frameSizePolicy = mock(Http2FrameSizePolicy.class);
+        when(writer.configuration()).thenReturn(configuration);
+        when(configuration.frameSizePolicy()).thenReturn(frameSizePolicy);
+        when(frameSizePolicy.maxFrameSize()).thenReturn(DEFAULT_MAX_FRAME_SIZE);
+
+        when(writer.writeRstStream(eq(ctx), anyInt(), anyLong(), any(ChannelPromise.class)))
+                .thenAnswer(new Answer<ChannelFuture>() {
+                    @Override
+                    public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                        return handlePromise(invocationOnMock, 3);
+                    }
+                });
+        when(writer.writeGoAway(any(ChannelHandlerContext.class), anyInt(), anyLong(), any(ByteBuf.class),
+                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
+            @Override
+            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                ReferenceCountUtil.release(invocationOnMock.getArgument(3));
+                ChannelPromise promise = invocationOnMock.getArgument(4);
+                goAwayPromises.offer(promise);
+                return promise;
+            }
+        });
+        Http2Connection connection = new DefaultHttp2Connection(false);
+        connection.remote().flowController(new DefaultHttp2RemoteFlowController(connection));
+        connection.local().flowController(new DefaultHttp2LocalFlowController(connection).frameWriter(writer));
+
+        DefaultHttp2ConnectionEncoder defaultEncoder =
+                new DefaultHttp2ConnectionEncoder(connection, writer);
+        encoder = new Http2MaxRstFrameLimitEncoder(defaultEncoder, 2, 1);
+        DefaultHttp2ConnectionDecoder decoder =
+                new DefaultHttp2ConnectionDecoder(connection, encoder, mock(Http2FrameReader.class));
+        Http2ConnectionHandler handler = new Http2ConnectionHandlerBuilder()
+                .frameListener(mock(Http2FrameListener.class))
+                .codec(decoder, encoder).build();
+
+        // Set LifeCycleManager on encoder and decoder
+        when(ctx.channel()).thenReturn(channel);
+        when(ctx.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
+        when(channel.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
+        when(executor.inEventLoop()).thenReturn(true);
+        doAnswer(new Answer<ChannelPromise>() {
+            @Override
+            public ChannelPromise answer(InvocationOnMock invocation) throws Throwable {
+                return newPromise();
+            }
+        }).when(ctx).newPromise();
+        when(ctx.executor()).thenReturn(executor);
+        when(channel.isActive()).thenReturn(false);
+        when(channel.config()).thenReturn(config);
+        when(channel.isWritable()).thenReturn(true);
+        when(channel.bytesBeforeUnwritable()).thenReturn(Long.MAX_VALUE);
+        when(config.getWriteBufferHighWaterMark()).thenReturn(Integer.MAX_VALUE);
+        when(config.getMessageSizeEstimator()).thenReturn(DefaultMessageSizeEstimator.DEFAULT);
+        ChannelMetadata metadata = new ChannelMetadata(false, 16);
+        when(channel.metadata()).thenReturn(metadata);
+        when(channel.unsafe()).thenReturn(unsafe);
+        handler.handlerAdded(ctx);
+    }
+
+    private ChannelPromise handlePromise(InvocationOnMock invocationOnMock, int promiseIdx) {
+        ChannelPromise promise = invocationOnMock.getArgument(promiseIdx);
+        return promise.setSuccess();
+    }
+
+    @AfterEach
+    public void teardown() {
+        // Close and release any buffered frames.
+        encoder.close();
+
+        // Notify all goAway ChannelPromise instances now as these will also release the retained ByteBuf for the
+        // debugData.
+        for (;;) {
+            ChannelPromise promise = goAwayPromises.poll();
+            if (promise == null) {
+                break;
+            }
+            promise.setSuccess();
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(Http2Error.class)
+    public void testLimitRst(Http2Error error) {
+        assertTrue(encoder.writeRstStream(ctx, 1, error.code(), newPromise()).isSuccess());
+        assertTrue(encoder.writeRstStream(ctx, 1, error.code(), newPromise()).isSuccess());
+        verifyFlushAndClose(0, false);
+        assertTrue(encoder.writeRstStream(ctx, 1, error.code(), newPromise()).isSuccess());
+        if (error == CANCEL || error == NO_ERROR) {
+            // CANCEL and NO_ERROR are ignored as these will not be caused by a stream error.
+            verifyFlushAndClose(0, false);
+        } else {
+            verifyFlushAndClose(1, true);
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(Http2Error.class)
+    public void testLimitRstReset(Http2Error error) throws Exception {
+        assertTrue(encoder.writeRstStream(ctx, 1, error.code(), newPromise()).isSuccess());
+        assertTrue(encoder.writeRstStream(ctx, 1, error.code(), newPromise()).isSuccess());
+        verifyFlushAndClose(0, false);
+        Thread.sleep(1000);
+        assertTrue(encoder.writeRstStream(ctx, 1, error.code(), newPromise()).isSuccess());
+        verifyFlushAndClose(0, false);
+    }
+
+    private void verifyFlushAndClose(int invocations, boolean failed) {
+        verify(ctx, atLeast(invocations)).flush();
+        verify(ctx, times(invocations)).close();
+        if (failed) {
+            verify(writer, times(1)).writeGoAway(eq(ctx), eq(Integer.MAX_VALUE), eq(ENHANCE_YOUR_CALM.code()),
+                    any(ByteBuf.class), any(ChannelPromise.class));
+        }
+    }
+
+    private ChannelPromise newPromise() {
+        return new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE);
+    }
+}
