Skip to content

Commit d2da15c

Browse files
authored
[HTTP2] Tolerate GoAway and Settings frames after connection close (swift-server#578)
1 parent f50bf98 commit d2da15c

File tree

3 files changed

+53
-6
lines changed

3 files changed

+53
-6
lines changed

Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2IdleHandler.swift

+26-6
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ extension HTTP2IdleHandler {
168168

169169
mutating func settingsReceived(_ settings: HTTP2Settings) -> Action {
170170
switch self.state {
171-
case .initialized, .closed:
171+
case .initialized:
172172
preconditionFailure("Invalid state: \(self.state)")
173173

174174
case .connected:
@@ -188,12 +188,17 @@ extension HTTP2IdleHandler {
188188

189189
case .closing:
190190
return .nothing
191+
192+
case .closed:
193+
// We may receive a Settings frame after we have called connection close, because of
194+
// packages being delivered from the incoming buffer.
195+
return .nothing
191196
}
192197
}
193198

194199
mutating func goAwayReceived() -> Action {
195200
switch self.state {
196-
case .initialized, .closed:
201+
case .initialized:
197202
preconditionFailure("Invalid state: \(self.state)")
198203

199204
case .connected:
@@ -206,6 +211,11 @@ extension HTTP2IdleHandler {
206211

207212
case .closing:
208213
return .notifyConnectionGoAwayReceived(close: false)
214+
215+
case .closed:
216+
// We may receive a GoAway frame after we have called connection close, because of
217+
// packages being delivered from the incoming buffer.
218+
return .nothing
209219
}
210220
}
211221

@@ -234,6 +244,9 @@ extension HTTP2IdleHandler {
234244

235245
mutating func streamCreated() -> Action {
236246
switch self.state {
247+
case .initialized, .connected:
248+
preconditionFailure("Invalid state: \(self.state)")
249+
237250
case .active(var openStreams, let maxStreams):
238251
openStreams += 1
239252
self.state = .active(openStreams: openStreams, maxStreams: maxStreams)
@@ -246,13 +259,18 @@ extension HTTP2IdleHandler {
246259
self.state = .closing(openStreams: openStreams, maxStreams: maxStreams)
247260
return .nothing
248261

249-
case .initialized, .connected, .closed:
250-
preconditionFailure("Invalid state: \(self.state)")
262+
case .closed:
263+
// We may receive a events after we have called connection close, because of
264+
// internal races. We should just ignore these cases.
265+
return .nothing
251266
}
252267
}
253268

254269
mutating func streamClosed() -> Action {
255270
switch self.state {
271+
case .initialized, .connected:
272+
preconditionFailure("Invalid state: \(self.state)")
273+
256274
case .active(var openStreams, let maxStreams):
257275
openStreams -= 1
258276
assert(openStreams >= 0)
@@ -269,8 +287,10 @@ extension HTTP2IdleHandler {
269287
self.state = .closing(openStreams: openStreams, maxStreams: maxStreams)
270288
return .nothing
271289

272-
case .initialized, .connected, .closed:
273-
preconditionFailure("Invalid state: \(self.state)")
290+
case .closed:
291+
// We may receive a events after we have called connection close, because of
292+
// internal races. We should just ignore these cases.
293+
return .nothing
274294
}
275295
}
276296
}

Tests/AsyncHTTPClientTests/HTTP2IdleHandlerTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ extension HTTP2IdleHandlerTests {
3535
("testCloseEventWhileNoOpenStreams", testCloseEventWhileNoOpenStreams),
3636
("testCloseEventWhileThereAreOpenStreams", testCloseEventWhileThereAreOpenStreams),
3737
("testGoAwayWhileThereAreOpenStreams", testGoAwayWhileThereAreOpenStreams),
38+
("testReceiveSettingsAndGoAwayAfterClientSideClose", testReceiveSettingsAndGoAwayAfterClientSideClose),
3839
]
3940
}
4041
}

Tests/AsyncHTTPClientTests/HTTP2IdleHandlerTests.swift

+26
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,32 @@ class HTTP2IdleHandlerTests: XCTestCase {
225225
}
226226
}
227227
}
228+
229+
func testReceiveSettingsAndGoAwayAfterClientSideClose() {
230+
let delegate = MockHTTP2IdleHandlerDelegate()
231+
let idleHandler = HTTP2IdleHandler(delegate: delegate, logger: Logger(label: "test"))
232+
let embedded = EmbeddedChannel(handlers: [idleHandler])
233+
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
234+
235+
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
236+
XCTAssertEqual(delegate.maxStreams, nil)
237+
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
238+
XCTAssertEqual(delegate.maxStreams, 10)
239+
240+
XCTAssertTrue(embedded.isActive)
241+
embedded.pipeline.triggerUserOutboundEvent(HTTPConnectionEvent.shutdownRequested, promise: nil)
242+
XCTAssertFalse(embedded.isActive)
243+
244+
let newSettingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 20)])))
245+
XCTAssertEqual(delegate.maxStreams, 10)
246+
XCTAssertNoThrow(try embedded.writeInbound(newSettingsFrame))
247+
XCTAssertEqual(delegate.maxStreams, 10, "Expected message to not be forwarded.")
248+
249+
let goAwayFrame = HTTP2Frame(streamID: HTTP2StreamID(0), payload: .goAway(lastStreamID: 2, errorCode: .http11Required, opaqueData: nil))
250+
XCTAssertEqual(delegate.goAwayReceived, false)
251+
XCTAssertNoThrow(try embedded.writeInbound(goAwayFrame))
252+
XCTAssertEqual(delegate.goAwayReceived, false, "Expected go away to not be forwarded.")
253+
}
228254
}
229255

230256
class MockHTTP2IdleHandlerDelegate: HTTP2IdleHandlerDelegate {

0 commit comments

Comments
 (0)