Skip to content

Commit 39ed0e7

Browse files
authored
Forward all reads before stream channel inactive (#317)
### Motivation In the AsyncHTTPClient the read event is not always forwarded right away. We have seen instances, in which we see a `HTTPClientError.remoteConnectionClosed` error, on requests that finish normally. swift-server/async-http-client#488 On deeper inspection, I noticed: If there is no unsatisfied read event, when a stream is closed, the pending reads are not forwarded. This can lead to response bytes being ignored on successful requests. NIOHTTP2 should behave as NIO and force forward all pending reads on channelInactive. ### Changes - Forward all pending reads on channelInactive even if no read event has hit the channel ### Result All requests will receive the complete request body.
1 parent 1aba777 commit 39ed0e7

File tree

3 files changed

+136
-2
lines changed

3 files changed

+136
-2
lines changed

Sources/NIOHTTP2/HTTP2StreamChannel.swift

+7-2
Original file line numberDiff line numberDiff line change
@@ -812,8 +812,13 @@ internal extension HTTP2StreamChannel {
812812
// Avoid emitting any WINDOW_UPDATE frames now that we're closed.
813813
self.windowManager.closed = true
814814

815-
// The stream is closed, we should aim to deliver any read frames we have for it.
816-
self.tryToRead()
815+
// The stream is closed, we should force forward all pending frames, even without
816+
// unsatisfied read, to ensure the handlers can see all frames before receiving
817+
// channelInactive.
818+
if self.pendingReads.count > 0 && self._isActive {
819+
self.unsatisfiedRead = false
820+
self.deliverPendingReads()
821+
}
817822

818823
if let reason = reason {
819824
// To receive from the network, it must be safe to force-unwrap here.

Tests/NIOHTTP2Tests/HTTP2FramePayloadStreamMultiplexerTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ extension HTTP2FramePayloadStreamMultiplexerTests {
7979
("testWindowUpdateIsNotEmittedAfterStreamIsClosedEvenOnLaterFrame", testWindowUpdateIsNotEmittedAfterStreamIsClosedEvenOnLaterFrame),
8080
("testStreamChannelSupportsSyncOptions", testStreamChannelSupportsSyncOptions),
8181
("testStreamErrorIsDeliveredToChannel", testStreamErrorIsDeliveredToChannel),
82+
("testPendingReadsAreFlushedEvenWithoutUnsatisfiedReadOnChannelInactive", testPendingReadsAreFlushedEvenWithoutUnsatisfiedReadOnChannelInactive),
8283
]
8384
}
8485
}

Tests/NIOHTTP2Tests/HTTP2FramePayloadStreamMultiplexerTests.swift

+128
Original file line numberDiff line numberDiff line change
@@ -1992,6 +1992,79 @@ final class HTTP2FramePayloadStreamMultiplexerTests: XCTestCase {
19921992
frames[0].assertHeadersFrame(endStream: false, streamID: 1, headers: goodHeaders, priority: nil, type: .request)
19931993
frames[1].assertHeadersFrame(endStream: false, streamID: 3, headers: badHeaders, priority: nil, type: .doNotValidate)
19941994
}
1995+
1996+
func testPendingReadsAreFlushedEvenWithoutUnsatisfiedReadOnChannelInactive() throws {
1997+
let goodHeaders = HPACKHeaders([
1998+
(":path", "/"), (":method", "GET"), (":scheme", "https"), (":authority", "localhost")
1999+
])
2000+
2001+
let multiplexer = HTTP2StreamMultiplexer(mode: .client, channel: self.channel) { channel in
2002+
XCTFail("Server push is unexpected")
2003+
return channel.eventLoop.makeSucceededFuture(())
2004+
}
2005+
XCTAssertNoThrow(try self.channel.pipeline.addHandler(multiplexer).wait())
2006+
2007+
// We need to activate the underlying channel here.
2008+
XCTAssertNoThrow(try self.channel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 80)).wait())
2009+
2010+
// Now create two child channels with error recording handlers in them. Save one, ignore the other.
2011+
let consumer = ReadAndFrameConsumer()
2012+
var childChannel: Channel!
2013+
multiplexer.createStreamChannel(promise: nil) { channel in
2014+
childChannel = channel
2015+
return channel.pipeline.addHandler(consumer)
2016+
}
2017+
self.channel.embeddedEventLoop.run()
2018+
2019+
let streamID = HTTP2StreamID(1)
2020+
2021+
let payload = HTTP2Frame.FramePayload.Headers(headers: goodHeaders, endStream: true)
2022+
XCTAssertNoThrow(try childChannel.writeAndFlush(HTTP2Frame.FramePayload.headers(payload)).wait())
2023+
2024+
let frames = try self.channel.sentFrames()
2025+
XCTAssertEqual(frames.count, 1)
2026+
frames.first?.assertHeadersFrameMatches(this: HTTP2Frame(streamID: streamID, payload: .headers(payload)))
2027+
2028+
XCTAssertEqual(consumer.readCount, 1)
2029+
2030+
// 1. pass header onwards
2031+
2032+
let responseHeaderPayload = HTTP2Frame.FramePayload.headers(.init(headers: [":status": "200"]))
2033+
XCTAssertNoThrow(try self.channel.writeInbound(HTTP2Frame(streamID: streamID, payload: responseHeaderPayload)))
2034+
XCTAssertEqual(consumer.receivedFrames.count, 1)
2035+
XCTAssertEqual(consumer.readCompleteCount, 1)
2036+
XCTAssertEqual(consumer.readCount, 2)
2037+
2038+
consumer.forwardRead = false
2039+
2040+
// 2. pass body onwards
2041+
2042+
let responseBody1 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(.init(string: "foo"))))
2043+
XCTAssertNoThrow(try self.channel.writeInbound(HTTP2Frame(streamID: streamID, payload: responseBody1)))
2044+
XCTAssertEqual(consumer.receivedFrames.count, 2)
2045+
XCTAssertEqual(consumer.readCompleteCount, 2)
2046+
XCTAssertEqual(consumer.readCount, 3)
2047+
XCTAssertEqual(consumer.readPending, true)
2048+
2049+
// 3. pass on more body - should not change a thing, since read is pending in consumer
2050+
2051+
let responseBody2 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(.init(string: "bar")), endStream: true))
2052+
XCTAssertNoThrow(try self.channel.writeInbound(HTTP2Frame(streamID: streamID, payload: responseBody2)))
2053+
XCTAssertEqual(consumer.receivedFrames.count, 2)
2054+
XCTAssertEqual(consumer.readCompleteCount, 2)
2055+
XCTAssertEqual(consumer.readCount, 3)
2056+
XCTAssertEqual(consumer.readPending, true)
2057+
2058+
// 4. signal stream is closed – this should force forward all pending frames
2059+
2060+
XCTAssertEqual(consumer.channelInactiveCount, 0)
2061+
self.channel.pipeline.fireUserInboundEventTriggered(StreamClosedEvent(streamID: streamID, reason: nil))
2062+
XCTAssertEqual(consumer.receivedFrames.count, 3)
2063+
XCTAssertEqual(consumer.readCompleteCount, 3)
2064+
XCTAssertEqual(consumer.readCount, 3)
2065+
XCTAssertEqual(consumer.channelInactiveCount, 1)
2066+
XCTAssertEqual(consumer.readPending, true)
2067+
}
19952068
}
19962069

19972070
private final class ErrorRecorder: ChannelInboundHandler {
@@ -2004,3 +2077,58 @@ private final class ErrorRecorder: ChannelInboundHandler {
20042077
context.fireErrorCaught(error)
20052078
}
20062079
}
2080+
2081+
private final class ReadAndFrameConsumer: ChannelInboundHandler, ChannelOutboundHandler {
2082+
typealias InboundIn = HTTP2Frame.FramePayload
2083+
typealias OutboundIn = HTTP2Frame.FramePayload
2084+
2085+
private(set) var receivedFrames: [HTTP2Frame.FramePayload] = []
2086+
private(set) var readCount = 0
2087+
private(set) var readCompleteCount = 0
2088+
private(set) var channelInactiveCount = 0
2089+
private(set) var readPending = false
2090+
2091+
var forwardRead = true {
2092+
didSet {
2093+
if self.forwardRead, self.readPending {
2094+
self.context.read()
2095+
self.readPending = false
2096+
}
2097+
}
2098+
}
2099+
2100+
var context: ChannelHandlerContext!
2101+
2102+
func handlerAdded(context: ChannelHandlerContext) {
2103+
self.context = context
2104+
}
2105+
2106+
func handlerRemoved(context: ChannelHandlerContext) {
2107+
self.context = context
2108+
}
2109+
2110+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
2111+
self.receivedFrames.append(self.unwrapInboundIn(data))
2112+
context.fireChannelRead(data)
2113+
}
2114+
2115+
func channelReadComplete(context: ChannelHandlerContext) {
2116+
self.readCompleteCount += 1
2117+
context.fireChannelReadComplete()
2118+
}
2119+
2120+
func channelInactive(context: ChannelHandlerContext) {
2121+
self.channelInactiveCount += 1
2122+
context.fireChannelInactive()
2123+
}
2124+
2125+
func read(context: ChannelHandlerContext) {
2126+
self.readCount += 1
2127+
if forwardRead {
2128+
context.read()
2129+
self.readPending = false
2130+
} else {
2131+
self.readPending = true
2132+
}
2133+
}
2134+
}

0 commit comments

Comments
 (0)