Skip to content

Commit 59bfb96

Browse files
authored
Add test for HTTP2 request with large header (#659)
Motivation We currently don't handle large headers well which trigger a channel writability change event. Modification Add failing (but currently skipped) tests which reproduces the issue Result We can reliably reproduce the large request header issue in an integration and unit test. Note that the actual fix is not included to make reviewing easier and will come in a follow up PR.
1 parent 67f99d1 commit 59bfb96

5 files changed

+78
-7
lines changed

Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ extension HTTP2ClientRequestHandlerTests {
3030
("testIdleReadTimeout", testIdleReadTimeout),
3131
("testIdleReadTimeoutIsCanceledIfRequestIsCanceled", testIdleReadTimeoutIsCanceledIfRequestIsCanceled),
3232
("testWriteHTTPHeadFails", testWriteHTTPHeadFails),
33+
("testChannelBecomesNonWritableDuringHeaderWrite", testChannelBecomesNonWritableDuringHeaderWrite),
3334
]
3435
}
3536
}

Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift

+29
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,33 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
345345
XCTAssertEqual(embedded.isActive, false)
346346
}
347347
}
348+
349+
func testChannelBecomesNonWritableDuringHeaderWrite() throws {
350+
try XCTSkipIf(true, "this currently fails and will be fixed in follow up PR")
351+
final class ChangeWritabilityOnFlush: ChannelOutboundHandler {
352+
typealias OutboundIn = Any
353+
func flush(context: ChannelHandlerContext) {
354+
context.flush()
355+
(context.channel as! EmbeddedChannel).isWritable = false
356+
context.fireChannelWritabilityChanged()
357+
}
358+
}
359+
let eventLoopGroup = EmbeddedEventLoopGroup(loops: 1)
360+
let eventLoop = eventLoopGroup.next() as! EmbeddedEventLoop
361+
let handler = HTTP2ClientRequestHandler(
362+
eventLoop: eventLoop
363+
)
364+
let channel = EmbeddedChannel(handlers: [
365+
ChangeWritabilityOnFlush(),
366+
handler,
367+
], loop: eventLoop)
368+
try channel.connect(to: .init(ipAddress: "127.0.0.1", port: 80)).wait()
369+
370+
let request = MockHTTPExecutableRequest()
371+
// non empty body is important to trigger this bug as we otherwise finish the request in a single flush
372+
request.requestFramingMetadata.body = .fixedSize(1)
373+
request.raiseErrorIfUnimplementedMethodIsCalled = false
374+
channel.writeAndFlush(request, promise: nil)
375+
XCTAssertEqual(request.events.map(\.kind), [.willExecuteRequest, .requestHeadSent])
376+
}
348377
}

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

+18-7
Original file line numberDiff line numberDiff line change
@@ -329,17 +329,32 @@ internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
329329
// supports http1.1 connections only, which can be either plain text or encrypted
330330
case http1_1(ssl: Bool = false, compress: Bool = false)
331331
// supports http1.1 and http2 connections which must be always encrypted
332-
case http2(compress: Bool)
332+
case http2(
333+
compress: Bool = false,
334+
settings: HTTP2Settings? = nil
335+
)
333336

334337
// supports request decompression and http response compression
335338
var compress: Bool {
336339
switch self {
337340
case .refuse:
338341
return false
339-
case .http1_1(ssl: _, compress: let compress), .http2(compress: let compress):
342+
case .http1_1(ssl: _, compress: let compress), .http2(compress: let compress, _):
340343
return compress
341344
}
342345
}
346+
347+
var httpSettings: HTTP2Settings {
348+
switch self {
349+
case .http1_1, .http2(_, nil), .refuse:
350+
return [
351+
HTTP2Setting(parameter: .maxConcurrentStreams, value: 10),
352+
HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize),
353+
]
354+
case .http2(_, .some(let customSettings)):
355+
return customSettings
356+
}
357+
}
343358
}
344359

345360
enum Proxy {
@@ -565,11 +580,7 @@ internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
565580
// Successful upgrade to HTTP/2. Let the user configure the pipeline.
566581
let http2Handler = NIOHTTP2Handler(
567582
mode: .server,
568-
initialSettings: [
569-
// TODO: make max concurrent streams configurable
570-
HTTP2Setting(parameter: .maxConcurrentStreams, value: 10),
571-
HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize),
572-
]
583+
initialSettings: self.mode.httpSettings
573584
)
574585
let multiplexer = HTTP2StreamMultiplexer(
575586
mode: .server,

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ extension HTTPClientTests {
144144
("testMassiveDownload", testMassiveDownload),
145145
("testShutdownWithFutures", testShutdownWithFutures),
146146
("testMassiveHeaderHTTP1", testMassiveHeaderHTTP1),
147+
("testMassiveHeaderHTTP2", testMassiveHeaderHTTP2),
147148
]
148149
}
149150
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+29
Original file line numberDiff line numberDiff line change
@@ -3378,4 +3378,33 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass {
33783378

33793379
XCTAssertNoThrow(try defaultClient.execute(request: request).wait())
33803380
}
3381+
3382+
func testMassiveHeaderHTTP2() throws {
3383+
try XCTSkipIf(true, "this currently crashes and will be fixed in follow up PR")
3384+
let bin = HTTPBin(.http2(settings: [
3385+
.init(parameter: .maxConcurrentStreams, value: 100),
3386+
.init(parameter: .maxHeaderListSize, value: 1024 * 256),
3387+
.init(parameter: .maxFrameSize, value: 1024 * 256),
3388+
]))
3389+
defer { XCTAssertNoThrow(try bin.shutdown()) }
3390+
3391+
let client = HTTPClient(
3392+
eventLoopGroupProvider: .shared(clientGroup),
3393+
configuration: .init(certificateVerification: .none)
3394+
)
3395+
3396+
defer { XCTAssertNoThrow(try client.syncShutdown()) }
3397+
3398+
var request = try HTTPClient.Request(url: bin.baseURL, method: .POST)
3399+
// add ~200 KB header
3400+
let headerValue = String(repeating: "0", count: 1024)
3401+
for headerID in 0..<200 {
3402+
request.headers.replaceOrAdd(name: "larg-header-\(headerID)", value: headerValue)
3403+
}
3404+
3405+
// non empty body is important to trigger this bug as we otherwise finish the request in a single flush
3406+
request.body = .byteBuffer(ByteBuffer(bytes: [0]))
3407+
3408+
XCTAssertNoThrow(try client.execute(request: request).wait())
3409+
}
33813410
}

0 commit comments

Comments
 (0)