Skip to content

Commit 67f99d1

Browse files
authored
Add test for HTTP1 request with large header (#658)
* Reproducer * Refactor test case * Refactor tests * Remove debugging artefacts * Fix typo * Fix formatting * Remove `promise?.succeed(())` * Rename `onRequestCompleted` to `onConnectionIdle`
1 parent 817d9aa commit 67f99d1

12 files changed

+298
-66
lines changed

Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
3535
didSet {
3636
if let newRequest = self.request {
3737
var requestLogger = newRequest.logger
38-
requestLogger[metadataKey: "ahc-connection-id"] = "\(self.connection.id)"
39-
requestLogger[metadataKey: "ahc-el"] = "\(self.connection.channel.eventLoop)"
38+
requestLogger[metadataKey: "ahc-connection-id"] = self.connectionIdLoggerMetadata
39+
requestLogger[metadataKey: "ahc-el"] = "\(self.eventLoop)"
4040
self.logger = requestLogger
4141

4242
if let idleReadTimeout = newRequest.requestOptions.idleReadTimeout {
@@ -59,15 +59,15 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
5959

6060
private let backgroundLogger: Logger
6161
private var logger: Logger
62+
private let eventLoop: EventLoop
63+
private let connectionIdLoggerMetadata: Logger.MetadataValue
6264

63-
let connection: HTTP1Connection
64-
let eventLoop: EventLoop
65-
66-
init(connection: HTTP1Connection, eventLoop: EventLoop, logger: Logger) {
67-
self.connection = connection
65+
var onConnectionIdle: () -> Void = {}
66+
init(eventLoop: EventLoop, backgroundLogger: Logger, connectionIdLoggerMetadata: Logger.MetadataValue) {
6867
self.eventLoop = eventLoop
69-
self.backgroundLogger = logger
70-
self.logger = self.backgroundLogger
68+
self.backgroundLogger = backgroundLogger
69+
self.logger = backgroundLogger
70+
self.connectionIdLoggerMetadata = connectionIdLoggerMetadata
7171
}
7272

7373
func handlerAdded(context: ChannelHandlerContext) {
@@ -108,6 +108,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
108108

109109
let action = self.state.writabilityChanged(writable: context.channel.isWritable)
110110
self.run(action, context: context)
111+
context.fireChannelWritabilityChanged()
111112
}
112113

113114
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
@@ -274,7 +275,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
274275
if shouldClose {
275276
context.close(promise: nil)
276277
} else {
277-
self.connection.taskCompleted()
278+
self.onConnectionIdle()
278279
}
279280

280281
oldRequest.succeedRequest(buffer)
@@ -286,7 +287,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
286287

287288
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: writePromise)
288289
case .informConnectionIsIdle:
289-
self.connection.taskCompleted()
290+
self.onConnectionIdle()
290291
oldRequest.succeedRequest(buffer)
291292
}
292293

@@ -303,7 +304,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
303304
oldRequest.fail(error)
304305

305306
case .informConnectionIsIdle:
306-
self.connection.taskCompleted()
307+
self.onConnectionIdle()
307308
oldRequest.fail(error)
308309

309310
case .failWritePromise(let writePromise):
@@ -328,6 +329,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
328329
// we must check if the request is still present here.
329330
guard let request = self.request else { return }
330331
request.requestHeadSent()
332+
331333
request.resumeRequestBodyStream()
332334
} else {
333335
context.write(self.wrapOutboundOut(.head(head)), promise: nil)
@@ -434,6 +436,11 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
434436
}
435437
}
436438

439+
#if swift(>=5.6)
440+
@available(*, unavailable)
441+
extension HTTP1ClientChannelHandler: Sendable {}
442+
#endif
443+
437444
extension HTTP1ClientChannelHandler: HTTPRequestExecutor {
438445
func writeRequestBodyPart(_ data: IOData, request: HTTPExecutableRequest, promise: EventLoopPromise<Void>?) {
439446
if self.eventLoop.inEventLoop {

Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1Connection.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,13 @@ final class HTTP1Connection {
133133
}
134134

135135
let channelHandler = HTTP1ClientChannelHandler(
136-
connection: self,
137136
eventLoop: channel.eventLoop,
138-
logger: logger
137+
backgroundLogger: logger,
138+
connectionIdLoggerMetadata: "\(self.id)"
139139
)
140+
channelHandler.onConnectionIdle = {
141+
self.taskCompleted()
142+
}
140143

141144
try sync.addHandler(channelHandler)
142145
} catch {

Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ extension HTTP1ClientChannelHandlerTests {
3333
("testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForDemand", testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForDemand),
3434
("testWriteHTTPHeadFails", testWriteHTTPHeadFails),
3535
("testHandlerClosesChannelIfLastActionIsSendEndAndItFails", testHandlerClosesChannelIfLastActionIsSendEndAndItFails),
36+
("testChannelBecomesNonWritableDuringHeaderWrite", testChannelBecomesNonWritableDuringHeaderWrite),
3637
]
3738
}
3839
}

Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,37 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
526526
XCTAssertTrue(error is FailEndHandler.Error)
527527
}
528528
}
529+
530+
func testChannelBecomesNonWritableDuringHeaderWrite() throws {
531+
try XCTSkipIf(true, "this currently fails and will be fixed in follow up PR")
532+
final class ChangeWritabilityOnFlush: ChannelOutboundHandler {
533+
typealias OutboundIn = Any
534+
func flush(context: ChannelHandlerContext) {
535+
context.flush()
536+
(context.channel as! EmbeddedChannel).isWritable = false
537+
context.fireChannelWritabilityChanged()
538+
}
539+
}
540+
let eventLoopGroup = EmbeddedEventLoopGroup(loops: 1)
541+
let eventLoop = eventLoopGroup.next() as! EmbeddedEventLoop
542+
let handler = HTTP1ClientChannelHandler(
543+
eventLoop: eventLoop,
544+
backgroundLogger: Logger(label: "no-op", factory: SwiftLogNoOpLogHandler.init),
545+
connectionIdLoggerMetadata: "test connection"
546+
)
547+
let channel = EmbeddedChannel(handlers: [
548+
ChangeWritabilityOnFlush(),
549+
handler,
550+
], loop: eventLoop)
551+
try channel.connect(to: .init(ipAddress: "127.0.0.1", port: 80)).wait()
552+
553+
let request = MockHTTPExecutableRequest()
554+
// non empty body is important to trigger this bug as we otherwise finish the request in a single flush
555+
request.requestFramingMetadata.body = .fixedSize(1)
556+
request.raiseErrorIfUnimplementedMethodIsCalled = false
557+
channel.writeAndFlush(request, promise: nil)
558+
XCTAssertEqual(request.events.map(\.kind), [.willExecuteRequest, .requestHeadSent])
559+
}
529560
}
530561

531562
class TestBackpressureWriter {

Tests/AsyncHTTPClientTests/HTTPClientBase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class XCTestCaseHTTPClientTestsBaseClass: XCTestCase {
3939
var backgroundLogStore: CollectEverythingLogHandler.LogStore!
4040

4141
var defaultHTTPBinURLPrefix: String {
42-
return "http://localhost:\(self.defaultHTTPBin.port)/"
42+
self.defaultHTTPBin.baseURL
4343
}
4444

4545
override func setUp() {

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,18 @@ internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
375375
return "https"
376376
}
377377
}()
378-
return "\(scheme)://localhost:\(self.port)/"
378+
let host: String = {
379+
switch self.socketAddress {
380+
case .v4:
381+
return self.socketAddress.ipAddress!
382+
case .v6:
383+
return "[\(self.socketAddress.ipAddress!)]"
384+
case .unixDomainSocket:
385+
return self.socketAddress.pathname!
386+
}
387+
}()
388+
389+
return "\(scheme)://\(host):\(self.port)/"
379390
}
380391

381392
private let mode: Mode

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ extension HTTPClientTests {
143143
("testRequestWithHeaderTransferEncodingIdentityDoesNotFail", testRequestWithHeaderTransferEncodingIdentityDoesNotFail),
144144
("testMassiveDownload", testMassiveDownload),
145145
("testShutdownWithFutures", testShutdownWithFutures),
146+
("testMassiveHeaderHTTP1", testMassiveHeaderHTTP1),
146147
]
147148
}
148149
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3363,4 +3363,19 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass {
33633363
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup))
33643364
XCTAssertNoThrow(try httpClient.shutdown().wait())
33653365
}
3366+
3367+
func testMassiveHeaderHTTP1() throws {
3368+
try XCTSkipIf(true, "this currently crashes and will be fixed in follow up PR")
3369+
var request = try HTTPClient.Request(url: defaultHTTPBin.baseURL, method: .POST)
3370+
// add ~64 KB header
3371+
let headerValue = String(repeating: "0", count: 1024)
3372+
for headerID in 0..<64 {
3373+
request.headers.replaceOrAdd(name: "larg-header-\(headerID)", value: headerValue)
3374+
}
3375+
3376+
// non empty body is important to trigger this bug as we otherwise finish the request in a single flush
3377+
request.body = .byteBuffer(ByteBuffer(bytes: [0]))
3378+
3379+
XCTAssertNoThrow(try defaultClient.execute(request: request).wait())
3380+
}
33663381
}

Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1StateTests.swift

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
3737
// for the first eight requests, the pool should try to create new connections.
3838

3939
for _ in 0..<8 {
40-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
40+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
4141
let request = HTTPConnectionPool.Request(mockRequest)
4242
let action = state.executeRequest(request)
4343
guard case .createConnection(let connectionID, let connectionEL) = action.connection else {
@@ -53,7 +53,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
5353
// the next eight requests should only be queued.
5454

5555
for _ in 0..<8 {
56-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
56+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
5757
let request = HTTPConnectionPool.Request(mockRequest)
5858
let action = state.executeRequest(request)
5959
guard case .none = action.connection else {
@@ -120,7 +120,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
120120
// for the first eight requests, the pool should try to create new connections.
121121

122122
for _ in 0..<8 {
123-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
123+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
124124
let request = HTTPConnectionPool.Request(mockRequest)
125125
let action = state.executeRequest(request)
126126
guard case .createConnection(let connectionID, let connectionEL) = action.connection else {
@@ -136,7 +136,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
136136
// the next eight requests should only be queued.
137137

138138
for _ in 0..<8 {
139-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
139+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
140140
let request = HTTPConnectionPool.Request(mockRequest)
141141
let action = state.executeRequest(request)
142142
guard case .none = action.connection else {
@@ -181,7 +181,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
181181
retryConnectionEstablishment: true
182182
)
183183

184-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
184+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
185185
let request = HTTPConnectionPool.Request(mockRequest)
186186

187187
let action = state.executeRequest(request)
@@ -239,7 +239,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
239239
retryConnectionEstablishment: true
240240
)
241241

242-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
242+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
243243
let request = HTTPConnectionPool.Request(mockRequest)
244244

245245
let executeAction = state.executeRequest(request)
@@ -276,7 +276,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
276276
retryConnectionEstablishment: true
277277
)
278278

279-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
279+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
280280
let request = HTTPConnectionPool.Request(mockRequest)
281281

282282
let executeAction = state.executeRequest(request)
@@ -310,7 +310,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
310310
XCTAssertEqual(cleanupContext.connectBackoff, [])
311311

312312
// 4. execute another request
313-
let finalMockRequest = MockHTTPRequest(eventLoop: elg.next())
313+
let finalMockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
314314
let finalRequest = HTTPConnectionPool.Request(finalMockRequest)
315315
let failAction = state.executeRequest(finalRequest)
316316
XCTAssertEqual(failAction.connection, .none)
@@ -339,7 +339,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
339339
return XCTFail("Expected to still have connections available")
340340
}
341341

342-
let mockRequest = MockHTTPRequest(eventLoop: eventLoop)
342+
let mockRequest = MockHTTPScheduableRequest(eventLoop: eventLoop)
343343
let request = HTTPConnectionPool.Request(mockRequest)
344344
let action = state.executeRequest(request)
345345

@@ -359,7 +359,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
359359
var queuer = MockRequestQueuer()
360360
for _ in 0..<100 {
361361
let eventLoop = elg.next()
362-
let mockRequest = MockHTTPRequest(eventLoop: eventLoop, requiresEventLoopForChannel: false)
362+
let mockRequest = MockHTTPScheduableRequest(eventLoop: eventLoop, requiresEventLoopForChannel: false)
363363
let request = HTTPConnectionPool.Request(mockRequest)
364364
let action = state.executeRequest(request)
365365

@@ -418,7 +418,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
418418

419419
// 10% of the cases enforce the eventLoop
420420
let elRequired = (0..<10).randomElement().flatMap { $0 == 0 ? true : false }!
421-
let mockRequest = MockHTTPRequest(eventLoop: reqEventLoop, requiresEventLoopForChannel: elRequired)
421+
let mockRequest = MockHTTPScheduableRequest(eventLoop: reqEventLoop, requiresEventLoopForChannel: elRequired)
422422
let request = HTTPConnectionPool.Request(mockRequest)
423423

424424
let action = state.executeRequest(request)
@@ -482,7 +482,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
482482
XCTAssertEqual(connections.parked, 8)
483483

484484
// close a leased connection == abort
485-
let mockRequest = MockHTTPRequest(eventLoop: elg.next())
485+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next())
486486
let request = HTTPConnectionPool.Request(mockRequest)
487487
guard let connectionToAbort = connections.newestParkedConnection else {
488488
return XCTFail("Expected to have a parked connection")
@@ -536,7 +536,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
536536
return XCTFail("Expected to still have connections available")
537537
}
538538

539-
let mockRequest = MockHTTPRequest(eventLoop: eventLoop)
539+
let mockRequest = MockHTTPScheduableRequest(eventLoop: eventLoop)
540540
let request = HTTPConnectionPool.Request(mockRequest)
541541
let action = state.executeRequest(request)
542542

@@ -553,7 +553,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
553553
for _ in 0..<100 {
554554
let eventLoop = elg.next()
555555

556-
let mockRequest = MockHTTPRequest(eventLoop: eventLoop, requiresEventLoopForChannel: false)
556+
let mockRequest = MockHTTPScheduableRequest(eventLoop: eventLoop, requiresEventLoopForChannel: false)
557557
let request = HTTPConnectionPool.Request(mockRequest)
558558
let action = state.executeRequest(request)
559559

@@ -667,7 +667,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
667667
retryConnectionEstablishment: true
668668
)
669669

670-
let mockRequest = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
670+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
671671
let request = HTTPConnectionPool.Request(mockRequest)
672672

673673
let executeAction = state.executeRequest(request)
@@ -706,7 +706,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
706706
retryConnectionEstablishment: true
707707
)
708708

709-
let mockRequest = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
709+
let mockRequest = MockHTTPScheduableRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
710710
let request = HTTPConnectionPool.Request(mockRequest)
711711

712712
let executeAction = state.executeRequest(request)
@@ -738,7 +738,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
738738
retryConnectionEstablishment: true
739739
)
740740

741-
let mockRequest = MockHTTPRequest(eventLoop: eventLoop.next(), requiresEventLoopForChannel: false)
741+
let mockRequest = MockHTTPScheduableRequest(eventLoop: eventLoop.next(), requiresEventLoopForChannel: false)
742742
let request = HTTPConnectionPool.Request(mockRequest)
743743

744744
let executeAction = state.executeRequest(request)
@@ -762,7 +762,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
762762
retryConnectionEstablishment: true
763763
)
764764

765-
let mockRequest1 = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
765+
let mockRequest1 = MockHTTPScheduableRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
766766
let request1 = HTTPConnectionPool.Request(mockRequest1)
767767

768768
let executeAction1 = state.executeRequest(request1)
@@ -773,7 +773,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
773773

774774
XCTAssertEqual(executeAction1.request, .scheduleRequestTimeout(for: request1, on: mockRequest1.eventLoop))
775775

776-
let mockRequest2 = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
776+
let mockRequest2 = MockHTTPScheduableRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
777777
let request2 = HTTPConnectionPool.Request(mockRequest2)
778778

779779
let executeAction2 = state.executeRequest(request2)

0 commit comments

Comments
 (0)