Repository: apple/swift-nio-http2
Pull request #518 (2025-06-29 15:19:39)

Add GlitchesMonitor
Add a `GlitchesMonitor` that will terminate a connection if too many stream resets are triggered.

gjcairo: The failing allocation test seems to be a regression elsewhere in NIO (see https://github.com/apple/swift-nio-http2/pull/5190)

Patch:
diff --git a/Sources/NIOHTTP2/ConnectionStateMachine/StateMachineResult.swift b/Sources/NIOHTTP2/ConnectionStateMachine/StateMachineResult.swift
index 85113d1..450279a 100644
--- a/Sources/NIOHTTP2/ConnectionStateMachine/StateMachineResult.swift
+++ b/Sources/NIOHTTP2/ConnectionStateMachine/StateMachineResult.swift
@@ -35,7 +35,11 @@ enum StateMachineResult {
 
     /// An error that transitions the entire connection into a fatal error state. This should cause
     /// emission of GOAWAY frames.
-    case connectionError(underlyingError: Error, type: HTTP2ErrorCode)
+    case connectionError(
+        underlyingError: Error,
+        type: HTTP2ErrorCode,
+        isMisbehavingPeer: Bool = false
+    )
 
     /// The frame itself was not valid, but it is also not an error. Drop the frame.
     case ignoreFrame
diff --git a/Sources/NIOHTTP2/GlitchesMonitor.swift b/Sources/NIOHTTP2/GlitchesMonitor.swift
new file mode 100644
index 0000000..b9e6a6c
--- /dev/null
+++ b/Sources/NIOHTTP2/GlitchesMonitor.swift
@@ -0,0 +1,71 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+struct GlitchesMonitor {
+    static var defaultMaximumGlitches: Int { 200 }
+    private var stateMachine: GlitchesMonitorStateMachine
+
+    init(maximumGlitches: Int = GlitchesMonitor.defaultMaximumGlitches) {
+        self.stateMachine = GlitchesMonitorStateMachine(maxGlitches: maximumGlitches)
+    }
+
+    mutating func processStreamError() throws {
+        switch self.stateMachine.recordEvent() {
+        case .belowLimit:
+            ()
+
+        case .exceededLimit:
+            throw NIOHTTP2Errors.excessiveNumberOfGlitches()
+        }
+    }
+}
+
+extension GlitchesMonitor {
+    private struct GlitchesMonitorStateMachine {
+        enum State {
+            case monitoring(numberOfGlitches: Int)
+            case glitchesExceeded
+        }
+
+        private var state: State
+        private let maxGlitches: Int
+
+        init(maxGlitches: Int) {
+            precondition(maxGlitches >= 0)
+            self.state = .monitoring(numberOfGlitches: 0)
+            self.maxGlitches = maxGlitches
+        }
+
+        enum RecordEventAction {
+            case belowLimit
+            case exceededLimit
+        }
+
+        mutating func recordEvent() -> RecordEventAction {
+            switch self.state {
+            case .monitoring(let numberOfGlitches):
+                if numberOfGlitches < self.maxGlitches {
+                    self.state = .monitoring(numberOfGlitches: numberOfGlitches &+ 1)
+                    return .belowLimit
+                } else {
+                    self.state = .glitchesExceeded
+                    return .exceededLimit
+                }
+
+            case .glitchesExceeded:
+                return .exceededLimit
+            }
+        }
+    }
+}
diff --git a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift
index e3f0081..cdb11bd 100644
--- a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift
+++ b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift
@@ -79,6 +79,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
     /// This object deploys heuristics to attempt to detect denial of service attacks.
     private var denialOfServiceValidator: DOSHeuristics<RealNIODeadlineClock>
 
+    private var glitchesMonitor: GlitchesMonitor
+
     /// The mode this handler is operating in.
     private let mode: ParserMode
 
@@ -236,6 +238,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
             maximumBufferedControlFrames: 10000,
             maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames,
             maximumRecentlyResetStreams: Self.defaultMaximumRecentlyResetFrames,
+            maximumConnectionGlitches: GlitchesMonitor.defaultMaximumGlitches,
             maximumResetFrameCount: 200,
             resetFrameCounterWindow: .seconds(30),
             maximumStreamErrorCount: 200,
@@ -273,6 +276,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
             maximumBufferedControlFrames: maximumBufferedControlFrames,
             maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames,
             maximumRecentlyResetStreams: Self.defaultMaximumRecentlyResetFrames,
+            maximumConnectionGlitches: GlitchesMonitor.defaultMaximumGlitches,
             maximumResetFrameCount: 200,
             resetFrameCounterWindow: .seconds(30),
             maximumStreamErrorCount: 200,
@@ -302,6 +306,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
             maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames,
             maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames,
             maximumRecentlyResetStreams: connectionConfiguration.maximumRecentlyResetStreams,
+            maximumConnectionGlitches: connectionConfiguration.maximumConnectionGlitches,
             maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
             resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
             maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
@@ -319,6 +324,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
         maximumBufferedControlFrames: Int,
         maximumSequentialContinuationFrames: Int,
         maximumRecentlyResetStreams: Int,
+        maximumConnectionGlitches: Int,
         maximumResetFrameCount: Int,
         resetFrameCounterWindow: TimeAmount,
         maximumStreamErrorCount: Int,
@@ -348,6 +354,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
         self.tolerateImpossibleStateTransitionsInDebugMode = false
         self.inboundStreamMultiplexerState = .uninitializedLegacy
         self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
+        self.glitchesMonitor = GlitchesMonitor(maximumGlitches: maximumConnectionGlitches)
     }
 
     /// Constructs a ``NIOHTTP2Handler``.
@@ -369,6 +376,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
     ///         against this DoS vector we put an upper limit on this rate. Defaults to 200.
     ///   - resetFrameCounterWindow:  Controls the sliding window used to enforce the maximum permitted reset frames rate. Too many may exhaust CPU resources. To protect
     ///         against this DoS vector we put an upper limit on this rate. 30 seconds.
+    ///   - maximumConnectionGlitches: Controls the maximum number of stream errors that can happen on a connection before the connection is reset. Defaults to 200.
     internal init(
         mode: ParserMode,
         initialSettings: HTTP2Settings = nioDefaultSettings,
@@ -382,7 +390,8 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
         maximumResetFrameCount: Int = 200,
         resetFrameCounterWindow: TimeAmount = .seconds(30),
         maximumStreamErrorCount: Int = 200,
-        streamErrorCounterWindow: TimeAmount = .seconds(30)
+        streamErrorCounterWindow: TimeAmount = .seconds(30),
+        maximumConnectionGlitches: Int = GlitchesMonitor.defaultMaximumGlitches
     ) {
         self.stateMachine = HTTP2ConnectionStateMachine(
             role: .init(mode),
@@ -408,6 +417,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
         self.tolerateImpossibleStateTransitionsInDebugMode = tolerateImpossibleStateTransitionsInDebugMode
         self.inboundStreamMultiplexerState = .uninitializedLegacy
         self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
+        self.glitchesMonitor = GlitchesMonitor(maximumGlitches: maximumConnectionGlitches)
     }
 
     public func handlerAdded(context: ChannelHandlerContext) {
@@ -600,32 +610,41 @@ extension NIOHTTP2Handler {
             self.inboundConnectionErrorTriggered(
                 context: context,
                 underlyingError: NIOHTTP2Errors.unableToParseFrame(),
-                reason: code
+                reason: code,
+                isMisbehavingPeer: false
             )
             return nil
         } catch is NIOHTTP2Errors.BadClientMagic {
             self.inboundConnectionErrorTriggered(
                 context: context,
                 underlyingError: NIOHTTP2Errors.badClientMagic(),
-                reason: .protocolError
+                reason: .protocolError,
+                isMisbehavingPeer: false
             )
             return nil
         } catch is NIOHTTP2Errors.ExcessivelyLargeHeaderBlock {
             self.inboundConnectionErrorTriggered(
                 context: context,
                 underlyingError: NIOHTTP2Errors.excessivelyLargeHeaderBlock(),
-                reason: .protocolError
+                reason: .protocolError,
+                isMisbehavingPeer: false
             )
             return nil
         } catch is NIOHTTP2Errors.ExcessiveContinuationFrames {
             self.inboundConnectionErrorTriggered(
                 context: context,
                 underlyingError: NIOHTTP2Errors.excessiveContinuationFrames(),
-                reason: .enhanceYourCalm
+                reason: .enhanceYourCalm,
+                isMisbehavingPeer: false
             )
             return nil
         } catch {
-            self.inboundConnectionErrorTriggered(context: context, underlyingError: error, reason: .internalError)
+            self.inboundConnectionErrorTriggered(
+                context: context,
+                underlyingError: error,
+                reason: .internalError,
+                isMisbehavingPeer: false
+            )
             return nil
         }
     }
@@ -733,6 +752,7 @@ extension NIOHTTP2Handler {
         }
 
         self.processDoSRisk(frame, result: &result)
+        self.processGlitches(result: &result)
         self.processStateChange(result.effect)
 
         let returnValue: FrameProcessResult
@@ -744,9 +764,14 @@ extension NIOHTTP2Handler {
         case .ignoreFrame:
             // Frame is good but no action needs to be taken.
             returnValue = .continue
-        case .connectionError(let underlyingError, let errorCode):
+        case .connectionError(let underlyingError, let errorCode, let isMisbehavingPeer):
             // We should stop parsing on received connection errors, the connection is going away anyway.
-            self.inboundConnectionErrorTriggered(context: context, underlyingError: underlyingError, reason: errorCode)
+            self.inboundConnectionErrorTriggered(
+                context: context,
+                underlyingError: underlyingError,
+                reason: errorCode,
+                isMisbehavingPeer: isMisbehavingPeer
+            )
             returnValue = .stop
         case .streamError(let streamID, let underlyingError, let errorCode):
             // We can continue parsing on stream errors in most cases, the frame is just ignored.
@@ -770,15 +795,20 @@ extension NIOHTTP2Handler {
     private func inboundConnectionErrorTriggered(
         context: ChannelHandlerContext,
         underlyingError: Error,
-        reason: HTTP2ErrorCode
+        reason: HTTP2ErrorCode,
+        isMisbehavingPeer: Bool
     ) {
         // A connection error brings the entire connection down. We attempt to write a GOAWAY frame, and then report this
         // error. It's possible that we'll be unable to write the GOAWAY frame, but that also just logs the error.
         // Because we don't know what data the user handled before we got this, we propose that they may have seen all of it.
         // The user may choose to fire a more specific error if they wish.
+
+        // If the peer is misbehaving, set the stream ID to the minimum allowed value (0).
+        // This will cause all open streams for this connection to be immediately terminated.
+        let streamID = isMisbehavingPeer ? HTTP2StreamID(0) : .maxID
         let goAwayFrame = HTTP2Frame(
             streamID: .rootStream,
-            payload: .goAway(lastStreamID: .maxID, errorCode: reason, opaqueData: nil)
+            payload: .goAway(lastStreamID: streamID, errorCode: reason, opaqueData: nil)
         )
         self.writeUnbufferedFrame(context: context, frame: goAwayFrame)
         self.flushIfNecessary(context: context)
@@ -818,7 +848,29 @@ extension NIOHTTP2Handler {
                 ()
             }
         } catch {
-            result.result = StateMachineResult.connectionError(underlyingError: error, type: .enhanceYourCalm)
+            result.result = StateMachineResult.connectionError(
+                underlyingError: error,
+                type: .enhanceYourCalm,
+                isMisbehavingPeer: true
+            )
+            result.effect = nil
+        }
+    }
+
+    private func processGlitches(result: inout StateMachineResultWithEffect) {
+        do {
+            switch result.result {
+            case .streamError:
+                try self.glitchesMonitor.processStreamError()
+            case .succeed, .ignoreFrame, .connectionError:
+                ()
+            }
+        } catch {
+            result.result = .connectionError(
+                underlyingError: error,
+                type: .enhanceYourCalm,
+                isMisbehavingPeer: true
+            )
             result.effect = nil
         }
     }
@@ -894,7 +946,12 @@ extension NIOHTTP2Handler {
             }
         } catch let error where error is NIOHTTP2Errors.ExcessiveOutboundFrameBuffering {
             self.inboundStreamMultiplexer?.processedFrame(frame)
-            self.inboundConnectionErrorTriggered(context: context, underlyingError: error, reason: .enhanceYourCalm)
+            self.inboundConnectionErrorTriggered(
+                context: context,
+                underlyingError: error,
+                reason: .enhanceYourCalm,
+                isMisbehavingPeer: false
+            )
         } catch {
             self.inboundStreamMultiplexer?.processedFrame(frame)
             promise?.fail(error)
@@ -968,7 +1025,7 @@ extension NIOHTTP2Handler {
         switch result.result {
         case .ignoreFrame:
             preconditionFailure("Cannot be asked to ignore outbound frames.")
-        case .connectionError(let underlyingError, _):
+        case .connectionError(let underlyingError, _, _):
             self.outboundConnectionErrorTriggered(context: context, promise: promise, underlyingError: underlyingError)
             return
         case .streamError(let streamID, let underlyingError, _):
@@ -1330,6 +1387,7 @@ extension NIOHTTP2Handler {
             maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames,
             maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames,
             maximumRecentlyResetStreams: connectionConfiguration.maximumRecentlyResetStreams,
+            maximumConnectionGlitches: connectionConfiguration.maximumConnectionGlitches,
             maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
             resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
             maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
@@ -1362,6 +1420,7 @@ extension NIOHTTP2Handler {
             maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames,
             maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames,
             maximumRecentlyResetStreams: connectionConfiguration.maximumRecentlyResetStreams,
+            maximumConnectionGlitches: connectionConfiguration.maximumConnectionGlitches,
             maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
             resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength,
             maximumStreamErrorCount: streamConfiguration.streamErrorRateLimit.maximumCount,
@@ -1386,6 +1445,17 @@ extension NIOHTTP2Handler {
         public var maximumBufferedControlFrames: Int = 10000
         public var maximumSequentialContinuationFrames: Int = NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames
         public var maximumRecentlyResetStreams: Int = NIOHTTP2Handler.defaultMaximumRecentlyResetFrames
+
+        /// The maximum number of glitches that are allowed on a connection before it's forcefully closed.
+        ///
+        /// A glitch is defined as some suspicious event on a connection, i.e., similar to a DoS attack.
+        /// A running count of the number of glitches occurring on each connection will be kept.
+        /// When the number of glitches reaches this threshold, the connection will be closed.
+        ///
+        /// For more information, see the relevant presentation of the 2024 HTTP Workshop:
+        /// https://github.com/HTTPWorkshop/workshop2024/blob/main/talks/1.%20Security/glitches.pdf
+        public var maximumConnectionGlitches: Int = GlitchesMonitor.defaultMaximumGlitches
+
         public init() {}
     }
 
diff --git a/Sources/NIOHTTP2/HTTP2Error.swift b/Sources/NIOHTTP2/HTTP2Error.swift
index ada5949..4a7a571 100644
--- a/Sources/NIOHTTP2/HTTP2Error.swift
+++ b/Sources/NIOHTTP2/HTTP2Error.swift
@@ -487,6 +487,18 @@ public enum NIOHTTP2Errors {
         ExcessiveEmptyDataFrames(file: file, line: line)
     }
 
+    /// Creates a ``ExcessiveNumberOfGlitches`` error with appropriate source context.
+    ///
+    /// - Parameters:
+    ///   - file: Source file of the caller.
+    ///   - line: Source line number of the caller.
+    public static func excessiveNumberOfGlitches(
+        file: String = #fileID,
+        line: UInt = #line
+    ) -> ExcessiveNumberOfGlitches {
+        ExcessiveNumberOfGlitches(file: file, line: line)
+    }
+
     /// Creates a ``ExcessivelyLargeHeaderBlock`` error with appropriate source context.
     ///
     /// - Parameters:
@@ -1832,6 +1844,31 @@ public enum NIOHTTP2Errors {
         }
     }
 
+    /// The remote peer has triggered too many glitches on this connection.
+    public struct ExcessiveNumberOfGlitches: NIOHTTP2Error {
+        private let file: String
+        private let line: UInt
+
+        /// The location where the error was thrown.
+        public var location: String {
+            _location(file: self.file, line: self.line)
+        }
+
+        @available(*, deprecated, renamed: "excessiveNumberOfGlitches")
+        public init() {
+            self.init(file: #fileID, line: #line)
+        }
+
+        fileprivate init(file: String, line: UInt) {
+            self.file = file
+            self.line = line
+        }
+
+        public static func == (lhs: ExcessiveNumberOfGlitches, rhs: ExcessiveNumberOfGlitches) -> Bool {
+            true
+        }
+    }
+
     /// The remote peer has sent a header block so large that ``NIOHTTP2`` refuses to buffer any more data than that.
     public struct ExcessivelyLargeHeaderBlock: NIOHTTP2Error {
         private let file: String


Test patch:
diff --git a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
index 5e59248..d703b72 100644
--- a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
+++ b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
@@ -30,15 +30,24 @@ func assertSucceeds(_ body: @autoclosure () -> StateMachineResult, file: StaticS
 @discardableResult
 func assertConnectionError(
     type: HTTP2ErrorCode,
+    isMisbehavingPeer: Bool = false,
     _ body: @autoclosure () -> StateMachineResult,
     file: StaticString = #filePath,
     line: UInt = #line
 ) -> Error? {
     switch body() {
-    case .connectionError(underlyingError: let error, type: type):
+    case .connectionError(
+        underlyingError: let error,
+        type: type,
+        isMisbehavingPeer: isMisbehavingPeer
+    ):
         return error
     case let result:
-        XCTFail("Expected connection error type \(type), got \(result)", file: (file), line: line)
+        XCTFail(
+            "Expected connection error of type \(type) and an isMisbehavingPeer value of \(isMisbehavingPeer), got \(result)",
+            file: (file),
+            line: line
+        )
         return nil
     }
 }
@@ -71,7 +80,7 @@ func assertBadStreamStateTransition(
     switch body().result {
     case .streamError(_, let underlyingError as NIOHTTP2Errors.BadStreamStateTransition, _):
         error = underlyingError
-    case .connectionError(let underlyingError as NIOHTTP2Errors.BadStreamStateTransition, _):
+    case .connectionError(let underlyingError as NIOHTTP2Errors.BadStreamStateTransition, _, _):
         error = underlyingError
     default:
         XCTFail("Unexpected result \(body().result)", file: (file), line: line)
@@ -102,11 +111,18 @@ func assertSucceeds(
 
 func assertConnectionError(
     type: HTTP2ErrorCode,
+    isMisBehavingPeer: Bool = false,
     _ body: @autoclosure () -> (StateMachineResultWithEffect, PostFrameOperation),
     file: StaticString = #filePath,
     line: UInt = #line
 ) {
-    assertConnectionError(type: type, body().0, file: (file), line: line)
+    assertConnectionError(
+        type: type,
+        isMisbehavingPeer: isMisBehavingPeer,
+        body().0,
+        file: (file),
+        line: line
+    )
 }
 
 func assertSucceeds(
@@ -120,13 +136,20 @@ func assertSucceeds(
 @discardableResult
 func assertConnectionError(
     type: HTTP2ErrorCode,
+    isMisbehavingPeer: Bool = false,
     _ body: @autoclosure () -> StateMachineResultWithEffect,
     file: StaticString = #filePath,
     line: UInt = #line
 ) -> Error? {
     // Errors must always lead to noChange.
     let result = body()
-    return assertConnectionError(type: type, result.result, file: (file), line: line)
+    return assertConnectionError(
+        type: type,
+        isMisbehavingPeer: isMisbehavingPeer,
+        result.result,
+        file: (file),
+        line: line
+    )
 }
 
 @discardableResult
diff --git a/Tests/NIOHTTP2Tests/GlitchesMonitorTests.swift b/Tests/NIOHTTP2Tests/GlitchesMonitorTests.swift
new file mode 100644
index 0000000..d6769d1
--- /dev/null
+++ b/Tests/NIOHTTP2Tests/GlitchesMonitorTests.swift
@@ -0,0 +1,36 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import XCTest
+
+@testable import NIOHTTP2
+
+class GlitchesMonitorTests: XCTestCase {
+    func testProcessStreamError() throws {
+        let maxGlitches: Int = 10
+        var monitor = GlitchesMonitor(maximumGlitches: maxGlitches)
+
+        // We accept up to `maxGlitches` glitches without erroring...
+        for _ in 1...maxGlitches {
+            XCTAssertNoThrow(try monitor.processStreamError())
+        }
+
+        // But any more glitches after that, we fail.
+        for _ in 1...5 {
+            XCTAssertThrowsError(try monitor.processStreamError()) { error in
+                XCTAssertNotNil(error as? NIOHTTP2Errors.ExcessiveNumberOfGlitches)
+            }
+        }
+    }
+}
diff --git a/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift b/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift
index 9f300f2..ece33c4 100644
--- a/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift
+++ b/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift
@@ -210,7 +210,8 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
         serverSettings: HTTP2Settings = nioDefaultSettings,
         maximumBufferedControlFrames: Int = 10000,
         maximumSequentialContinuationFrames: Int = 5,
-        withMultiplexerCallback multiplexerCallback: NIOChannelInitializer? = nil
+        withMultiplexerCallback multiplexerCallback: NIOChannelInitializer? = nil,
+        maximumConnectionGlitches: Int = 10
     ) throws {
         XCTAssertNoThrow(
             try self.clientChannel.pipeline.syncOperations.addHandler(
@@ -228,7 +229,8 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
                     mode: .server,
                     initialSettings: serverSettings,
                     maximumBufferedControlFrames: maximumBufferedControlFrames,
-                    maximumSequentialContinuationFrames: maximumSequentialContinuationFrames
+                    maximumSequentialContinuationFrames: maximumSequentialContinuationFrames,
+                    maximumConnectionGlitches: maximumConnectionGlitches
                 )
             )
         )
@@ -1977,8 +1979,11 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
             XCTFail("Did not receive server GOAWAY frame")
             return
         }
+
+        // The last stream ID should be 0 because we have detected this peer is misbehaving and
+        // all streams should be closed.
         goaway.assertGoAwayFrame(
-            lastStreamID: .maxID,
+            lastStreamID: 0,
             errorCode: UInt32(HTTP2ErrorCode.enhanceYourCalm.networkCode),
             opaqueData: nil
         )
@@ -2055,8 +2060,11 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
             XCTFail("Did not receive server GOAWAY frame")
             return
         }
+
+        // The last stream ID should be 0 because we have detected this peer is misbehaving and
+        // all streams should be closed.
         goaway.assertGoAwayFrame(
-            lastStreamID: .maxID,
+            lastStreamID: 0,
             errorCode: UInt32(HTTP2ErrorCode.enhanceYourCalm.networkCode),
             opaqueData: nil
         )