Skip to content

Commit aeff23a

Browse files
authored
Fix last peer initiated stream ID when quiescing (#1700)
Motivation: The idle handler records the ID of the last stream created by the remote peer and uses it when sending GOAWAY frames. In most paths this was correctly updated according to the role the handler played in the connection and the stream ID. However, in the quiescing state it was unconditionally updated. This can lead to cases where the client attempts to send a GOAWAY with a client initiated stream ID: this is invalid and NIO HTTP/2 treats it as a connection level error, closing all open streams. Modification: - Conditionally update the last peer initiated stream ID when quiescing Result: Fewer connection errors
1 parent 765ff32 commit aeff23a

File tree

2 files changed

+37
-1
lines changed

2 files changed

+37
-1
lines changed

Sources/GRPC/GRPCIdleHandlerStateMachine.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,15 @@ struct GRPCIdleHandlerStateMachine {
275275
operations.cancelIdleTask(state.idleTask)
276276

277277
case var .quiescing(state):
278-
state.lastPeerInitiatedStreamID = streamID
278+
switch state.role {
279+
case .client where streamID.isServerInitiated:
280+
state.lastPeerInitiatedStreamID = streamID
281+
case .server where streamID.isClientInitiated:
282+
state.lastPeerInitiatedStreamID = streamID
283+
default:
284+
()
285+
}
286+
279287
state.openStreams += 1
280288
self.state = .quiescing(state)
281289

Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,34 @@ class GRPCIdleHandlerStateMachineTests: GRPCTestCase {
535535
_ = stateMachine.channelInactive()
536536
stateMachine.ratchetDownGoAwayStreamID().assertDoNothing()
537537
}
538+
539+
func testStreamIDWhenQuiescing() {
540+
var stateMachine = self.makeClientStateMachine()
541+
let op1 = stateMachine.receiveSettings([])
542+
op1.assertConnectionManager(.ready)
543+
544+
// Open a stream so we enter quiescing when receiving the GOAWAY.
545+
let op2 = stateMachine.streamCreated(withID: 1)
546+
op2.assertDoNothing()
547+
548+
let op3 = stateMachine.receiveGoAway()
549+
op3.assertConnectionManager(.quiescing)
550+
551+
// Create a new stream. This can happen if the GOAWAY races with opening the stream; HTTP2 will
552+
// open and then close the stream with an error.
553+
let op4 = stateMachine.streamCreated(withID: 3)
554+
op4.assertDoNothing()
555+
556+
// Close the newly opened stream.
557+
let op5 = stateMachine.streamClosed(withID: 3)
558+
op5.assertDoNothing()
559+
560+
// Close the original stream.
561+
let op6 = stateMachine.streamClosed(withID: 1)
562+
// Now we can send a GOAWAY with stream ID zero (we're the client and the server didn't open
563+
// any streams).
564+
XCTAssertEqual(op6.sendGoAwayWithLastPeerInitiatedStreamID, 0)
565+
}
538566
}
539567

540568
extension GRPCIdleHandlerStateMachine.Operations {

0 commit comments

Comments
 (0)