@@ -4436,4 +4436,174 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass {
44364436 request. setBasicAuth ( username: " foo " , password: " bar " )
44374437 XCTAssertEqual ( request. headers. first ( name: " Authorization " ) , " Basic Zm9vOmJhcg== " )
44384438 }
4439+
4440+ func runBaseTestForHTTP1ConnectionDebugInitializer( ssl: Bool ) {
4441+ let connectionDebugInitializerUtil = CountingDebugInitializerUtil ( )
4442+
4443+ // Initializing even with just `http1_1ConnectionDebugInitializer` (rather than manually
4444+ // modifying `config`) to ensure that the matching `init` actually wires up this argument
4445+ // with the respective property. This is necessary as these parameters are defaulted and can
4446+ // be easy to miss.
4447+ var config = HTTPClient . Configuration (
4448+ http1_1ConnectionDebugInitializer: { channel in
4449+ connectionDebugInitializerUtil. initialize ( channel: channel)
4450+ }
4451+ )
4452+ config. httpVersion = . http1Only
4453+
4454+ if ssl {
4455+ config. tlsConfiguration = . clientDefault
4456+ config. tlsConfiguration? . certificateVerification = . none
4457+ }
4458+
4459+ let higherConnectTimeout = CountingDebugInitializerUtil . duration + . milliseconds( 100 )
4460+ var configWithHigherTimeout = config
4461+ configWithHigherTimeout. timeout = . init( connect: higherConnectTimeout)
4462+
4463+ let clientWithHigherTimeout = HTTPClient (
4464+ eventLoopGroupProvider: . singleton,
4465+ configuration: configWithHigherTimeout,
4466+ backgroundActivityLogger: Logger (
4467+ label: " HTTPClient " ,
4468+ factory: StreamLogHandler . standardOutput ( label: )
4469+ )
4470+ )
4471+ defer { XCTAssertNoThrow ( try clientWithHigherTimeout. syncShutdown ( ) ) }
4472+
4473+ let bin = HTTPBin ( . http1_1( ssl: ssl, compress: false ) )
4474+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
4475+
4476+ let scheme = ssl ? " https " : " http "
4477+
4478+ for _ in 0 ..< 3 {
4479+ XCTAssertNoThrow (
4480+ try clientWithHigherTimeout. get ( url: " \( scheme) ://localhost: \( bin. port) /get " ) . wait ( )
4481+ )
4482+ }
4483+
4484+ // Even though multiple requests were made, the connection debug initializer must be called
4485+ // only once.
4486+ XCTAssertEqual ( connectionDebugInitializerUtil. executionCount, 1 )
4487+
4488+ let lowerConnectTimeout = CountingDebugInitializerUtil . duration - . milliseconds( 100 )
4489+ var configWithLowerTimeout = config
4490+ configWithLowerTimeout. timeout = . init( connect: lowerConnectTimeout)
4491+
4492+ let clientWithLowerTimeout = HTTPClient (
4493+ eventLoopGroupProvider: . singleton,
4494+ configuration: configWithLowerTimeout,
4495+ backgroundActivityLogger: Logger (
4496+ label: " HTTPClient " ,
4497+ factory: StreamLogHandler . standardOutput ( label: )
4498+ )
4499+ )
4500+ defer { XCTAssertNoThrow ( try clientWithLowerTimeout. syncShutdown ( ) ) }
4501+
4502+ XCTAssertThrowsError (
4503+ try clientWithLowerTimeout. get ( url: " \( scheme) ://localhost: \( bin. port) /get " ) . wait ( )
4504+ ) {
4505+ XCTAssertEqual ( $0 as? HTTPClientError , . connectTimeout)
4506+ }
4507+ }
4508+
4509+ func testHTTP1PlainTextConnectionDebugInitializer( ) {
4510+ runBaseTestForHTTP1ConnectionDebugInitializer ( ssl: false )
4511+ }
4512+
4513+ func testHTTP1EncryptedConnectionDebugInitializer( ) {
4514+ runBaseTestForHTTP1ConnectionDebugInitializer ( ssl: true )
4515+ }
4516+
4517+ func testHTTP2ConnectionAndStreamChannelDebugInitializers( ) {
4518+ let connectionDebugInitializerUtil = CountingDebugInitializerUtil ( )
4519+ let streamChannelDebugInitializerUtil = CountingDebugInitializerUtil ( )
4520+
4521+ // Initializing even with just `http2ConnectionDebugInitializer` and
4522+ // `http2StreamChannelDebugInitializer` (rather than manually modifying `config`) to ensure
4523+ // that the matching `init` actually wires up these arguments with the respective
4524+ // properties. This is necessary as these parameters are defaulted and can be easy to miss.
4525+ var config = HTTPClient . Configuration (
4526+ http2ConnectionDebugInitializer: { channel in
4527+ connectionDebugInitializerUtil. initialize ( channel: channel)
4528+ } ,
4529+ http2StreamChannelDebugInitializer: { channel in
4530+ streamChannelDebugInitializerUtil. initialize ( channel: channel)
4531+ }
4532+ )
4533+ config. tlsConfiguration = . clientDefault
4534+ config. tlsConfiguration? . certificateVerification = . none
4535+ config. httpVersion = . automatic
4536+
4537+ let higherConnectTimeout = CountingDebugInitializerUtil . duration + . milliseconds( 100 )
4538+ var configWithHigherTimeout = config
4539+ configWithHigherTimeout. timeout = . init( connect: higherConnectTimeout)
4540+
4541+ let clientWithHigherTimeout = HTTPClient (
4542+ eventLoopGroupProvider: . singleton,
4543+ configuration: configWithHigherTimeout,
4544+ backgroundActivityLogger: Logger (
4545+ label: " HTTPClient " ,
4546+ factory: StreamLogHandler . standardOutput ( label: )
4547+ )
4548+ )
4549+ defer { XCTAssertNoThrow ( try clientWithHigherTimeout. syncShutdown ( ) ) }
4550+
4551+ let bin = HTTPBin ( . http2( compress: false ) )
4552+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
4553+
4554+ let numberOfRequests = 3
4555+
4556+ for _ in 0 ..< numberOfRequests {
4557+ XCTAssertNoThrow (
4558+ try clientWithHigherTimeout. get ( url: " https://localhost: \( bin. port) /get " ) . wait ( )
4559+ )
4560+ }
4561+
4562+ // Even though multiple requests were made, the connection debug initializer must be called
4563+ // only once.
4564+ XCTAssertEqual ( connectionDebugInitializerUtil. executionCount, 1 )
4565+
4566+ // The stream channel debug initializer must be called only as much as the number of
4567+ // requests made.
4568+ XCTAssertEqual ( streamChannelDebugInitializerUtil. executionCount, numberOfRequests)
4569+
4570+ let lowerConnectTimeout = CountingDebugInitializerUtil . duration - . milliseconds( 100 )
4571+ var configWithLowerTimeout = config
4572+ configWithLowerTimeout. timeout = . init( connect: lowerConnectTimeout)
4573+
4574+ let clientWithLowerTimeout = HTTPClient (
4575+ eventLoopGroupProvider: . singleton,
4576+ configuration: configWithLowerTimeout,
4577+ backgroundActivityLogger: Logger (
4578+ label: " HTTPClient " ,
4579+ factory: StreamLogHandler . standardOutput ( label: )
4580+ )
4581+ )
4582+ defer { XCTAssertNoThrow ( try clientWithLowerTimeout. syncShutdown ( ) ) }
4583+
4584+ XCTAssertThrowsError (
4585+ try clientWithLowerTimeout. get ( url: " https://localhost: \( bin. port) /get " ) . wait ( )
4586+ ) {
4587+ XCTAssertEqual ( $0 as? HTTPClientError , . connectTimeout)
4588+ }
4589+ }
4590+ }
4591+
4592+ final class CountingDebugInitializerUtil : Sendable {
4593+ private let _executionCount = NIOLockedValueBox < Int > ( 0 )
4594+ var executionCount : Int { self . _executionCount. withLockedValue { $0 } }
4595+
4596+ /// The minimum time to spend running the debug initializer.
4597+ static let duration : TimeAmount = . milliseconds( 300 )
4598+
4599+ /// The actual debug initializer.
4600+ func initialize( channel: Channel ) -> EventLoopFuture < Void > {
4601+ self . _executionCount. withLockedValue { $0 += 1 }
4602+
4603+ let someScheduledTask = channel. eventLoop. scheduleTask ( in: Self . duration) {
4604+ channel. eventLoop. makeSucceededVoidFuture ( )
4605+ }
4606+
4607+ return someScheduledTask. futureResult. flatMap { $0 }
4608+ }
44394609}
0 commit comments