Skip to content

Commit c095189

Browse files
authored
Trailers sent on idle streams deserve to be sent too. (#25)
Motivation: Due to nghttp2's weird data provider model, all trailers go through a similar send path as data frames. This includes the fact that the data sender can go "idle". If the data sender is idle, and the user sends trailers, we don't bother to tell nghttp2 to ask us for more data, so the trailers sit there forever, feeling sad. Modifications: - Correctly restart the data provider when sending data. Result: Trailers will be emitted when the stream is idle.
1 parent d24bb5d commit c095189

File tree

4 files changed

+45
-1
lines changed

4 files changed

+45
-1
lines changed

Sources/NIOHTTP2/NGHTTP2Session.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,14 @@ class NGHTTP2Session {
693693
// TODO(cory): Error handling.
694694
precondition(isEndStream)
695695
streamData.dataProvider.bufferEOF(trailers: headers)
696+
697+
if case .pending = streamData.dataProvider.state {
698+
// The data provider is currently in the pending state, we need to tell nghttp2 it's active again so
699+
// the trailers get emitted.
700+
let rc = nghttp2_session_resume_data(self.session, frame.streamID.networkStreamID!)
701+
precondition(rc == 0)
702+
streamData.dataProvider.didResume()
703+
}
696704
return
697705
}
698706

Tests/NIOHTTP2Tests/SimpleClientServerTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ extension SimpleClientServerTests {
4848
("testFailingUnflushedWritesForGoaway", testFailingUnflushedWritesForGoaway),
4949
("testFailingFlushedWritesForGoaway", testFailingFlushedWritesForGoaway),
5050
("testSendingTrailers", testSendingTrailers),
51+
("testSendingTrailersWhenDataProviderIsIdle", testSendingTrailersWhenDataProviderIsIdle),
5152
("test1XXResponseHeaderFields", test1XXResponseHeaderFields),
5253
("testPriorityFramesAreNotEmitted", testPriorityFramesAreNotEmitted),
5354
("testStreamClosedWithNoError", testStreamClosedWithNoError),

Tests/NIOHTTP2Tests/SimpleClientServerTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,41 @@ class SimpleClientServerTests: XCTestCase {
10061006
XCTAssertNoThrow(try self.serverChannel.finish())
10071007
}
10081008

1009+
func testSendingTrailersWhenDataProviderIsIdle() throws {
1010+
// Begin by getting the connection up.
1011+
try self.basicHTTP2Connection()
1012+
1013+
// We're now going to try to send a request from the client to the server. This request will be one headers frame, one
1014+
// data frame, and then another headers frame for trailers. The headers frame for trailers will be sent late, to
1015+
// reproduce https://github.com/apple/swift-nio-http2/issues/24.
1016+
let headers = HTTPHeaders([(":path", "/"), (":method", "POST"), (":scheme", "https"), (":authority", "localhost")])
1017+
let trailers = HTTPHeaders([("x-trailers-field", "true")])
1018+
var requestBody = self.clientChannel.allocator.buffer(capacity: 128)
1019+
requestBody.write(staticString: "A simple HTTP/2 request.")
1020+
1021+
let clientStreamID = HTTP2StreamID()
1022+
let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
1023+
let reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
1024+
var trailerFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(trailers))
1025+
trailerFrame.flags.insert(.endStream)
1026+
1027+
let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
1028+
1029+
// Now we can send the next trailers.
1030+
XCTAssertNoThrow(try self.assertFramesRoundTrip(frames: [trailerFrame], sender: self.clientChannel, receiver: self.serverChannel))
1031+
1032+
// Let's send a quick response back. This response should also contain trailers.
1033+
let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
1034+
let respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
1035+
var respTrailersFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(trailers))
1036+
respTrailersFrame.flags.insert(.endStream)
1037+
XCTAssertNoThrow(try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel))
1038+
XCTAssertNoThrow(try self.assertFramesRoundTrip(frames: [respTrailersFrame], sender: self.serverChannel, receiver: self.clientChannel))
1039+
1040+
XCTAssertNoThrow(try self.clientChannel.finish())
1041+
XCTAssertNoThrow(try self.serverChannel.finish())
1042+
}
1043+
10091044
func test1XXResponseHeaderFields() throws {
10101045
// Begin by getting the connection up.
10111046
try self.basicHTTP2Connection()

Tests/NIOHTTP2Tests/TestUtilities.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ extension XCTestCase {
119119
var receivedFrames = [HTTP2Frame]()
120120

121121
for frame in frames {
122-
let receivedFrame = try receiver.assertReceivedFrame()
122+
let receivedFrame = try receiver.assertReceivedFrame(file: file, line: line)
123123
receivedFrame.assertFrameMatches(this: frame, file: file, line: line)
124124
receivedFrames.append(receivedFrame)
125125
}

0 commit comments

Comments
 (0)