Skip to content

Commit 35b0b8f

Browse files
authored
Fix bug where client ignores HEADERS frames on open stream when locally quiescing (#445)
Motivation: Currently, HEADERS frames are ignored by the client on a client-initiated stream after the client sends a GOAWAY. As per [Section 6.8 (GOAWAY) in RFC 9113](https://httpwg.org/specs/rfc9113#GOAWAY): _"Once the GOAWAY is sent, the sender will ignore frames sent on streams **initiated by the receiver** if the stream has an identifier higher than the included last stream identifier."_ In this case, the client (sender) should **not** ignore the HEADERS frame as the stream is initiated by the _client_, not the receiver. Modifications: Fixed the condition in the `receiveHeaders` function (given `LocallyQuiescingState`) in `ReceivingHeadersState.swift` to work for _both_ the client and server roles -- before this change, the condition is only correct when `self.role == ConnectionRole.server` Result: HEADERS frames are not ignored by the client on a client-initiated stream when locally quiescing.
1 parent ab04d05 commit 35b0b8f

File tree

3 files changed

+29
-3
lines changed

3 files changed

+29
-3
lines changed

Sources/NIOHTTP2/ConnectionStateMachine/ConnectionStateMachine.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1755,7 +1755,7 @@ extension HTTP2StreamID {
17551755

17561756

17571757
/// A simple protocol that provides helpers that apply to all connection states that keep track of a role.
1758-
private protocol ConnectionStateWithRole {
1758+
protocol ConnectionStateWithRole {
17591759
var role: HTTP2ConnectionStateMachine.ConnectionRole { get }
17601760
}
17611761

Sources/NIOHTTP2/ConnectionStateMachine/FrameReceivingStates/ReceivingHeadersState.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import NIOHPACK
1717
/// can validly accept headers.
1818
///
1919
/// This protocol should only be conformed to by states for the HTTP/2 connection state machine.
20-
protocol ReceivingHeadersState: HasFlowControlWindows, HasLocalExtendedConnectSettings, HasRemoteExtendedConnectSettings {
20+
protocol ReceivingHeadersState: HasFlowControlWindows, HasLocalExtendedConnectSettings, HasRemoteExtendedConnectSettings, ConnectionStateWithRole {
2121
var role: HTTP2ConnectionStateMachine.ConnectionRole { get }
2222

2323
var headerBlockValidation: HTTP2ConnectionStateMachine.ValidationState { get }
@@ -74,7 +74,10 @@ extension ReceivingHeadersState where Self: LocallyQuiescingState {
7474
let localSupportsExtendedConnect = self.localSupportsExtendedConnect
7575
let remoteSupportsExtendedConnect = self.remoteSupportsExtendedConnect
7676

77-
if streamID.mayBeInitiatedBy(.client) && streamID > self.lastRemoteStreamID {
77+
// We are in `LocallyQuiescingState`. The sender of the GOAWAY is `self.role`, so the remote peer is the inverse of `self.role`.
78+
let remotePeer = self.peerRole
79+
80+
if streamID.mayBeInitiatedBy(remotePeer) && streamID > self.lastRemoteStreamID {
7881
return StateMachineResultWithEffect(result: .ignoreFrame, effect: nil)
7982
}
8083

Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,29 @@ class ConnectionStateMachineTests: XCTestCase {
985985
XCTAssertTrue(self.client.fullyQuiesced)
986986
}
987987

988+
func testHeadersOnOpenStreamLocallyQuiescing() {
989+
let streamOne = HTTP2StreamID(1)
990+
991+
self.exchangePreamble()
992+
993+
// Client sends headers
994+
assertSucceeds(client.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: false))
995+
assertSucceeds(server.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.requestHeaders, isEndStreamSet: false))
996+
997+
// Server responds with headers
998+
assertSucceeds(server.sendHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.responseHeaders, isEndStreamSet: false))
999+
assertSucceeds(client.receiveHeaders(streamID: streamOne, headers: ConnectionStateMachineTests.responseHeaders, isEndStreamSet: false))
1000+
1001+
// Client sends a GOAWAY with lastStreamID = 0
1002+
assertGoawaySucceeds(client.sendGoaway(lastStreamID: 0), droppingStreams: nil)
1003+
assertGoawaySucceeds(server.receiveGoaway(lastStreamID: 0), droppingStreams: nil)
1004+
1005+
// Server sends a header frame with end stream = true
1006+
let headerFrame = HPACKHeaders([("content-length", "0")])
1007+
assertSucceeds(server.sendHeaders(streamID: streamOne, headers: headerFrame, isEndStreamSet: true))
1008+
assertSucceeds(client.receiveHeaders(streamID: streamOne, headers: headerFrame, isEndStreamSet: true))
1009+
}
1010+
9881011
func testImplicitConnectionCompletion() {
9891012
// Connections can become totally idle by way of the server quiescing the client, and then having no outstanding streams.
9901013
// This test validates that we spot it and consider the connection closed at this stage.

0 commit comments

Comments
 (0)