Skip to content

Commit 9e26b5e

Browse files
committedJan 21, 2025
Add "debug initializer" hook for channels
Motivation: As requested in swift-server#596, it can be handy to have a lower-level access to channels (HTTP/1 connection, HTTP/2 connection, or HTTP/2 stream) to enable a more fine-grained interaction for, say, observability, testing, etc. Modifications: - Add 3 new properties (`http1_1ConnectionDebugInitializer`, `http2ConnectionDebugInitializer` and `http2StreamChannelDebugInitializer`) to `HTTPClient.Configuration` with access to the respective channels. These properties are of `Optional` type `@Sendable (Channel) -> EventLoopFuture<Void>` and are called when creating a connection/stream. Result: Provides APIs for a lower-level access to channels.
1 parent e69318d commit 9e26b5e

File tree

5 files changed

+233
-21
lines changed

5 files changed

+233
-21
lines changed
 

‎Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2Connection.swift

+24-6
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,21 @@ final class HTTP2Connection {
141141
return connection._start0().map { maxStreams in (connection, maxStreams) }
142142
}
143143

144-
func executeRequest(_ request: HTTPExecutableRequest) {
144+
func executeRequest(
145+
_ request: HTTPExecutableRequest,
146+
streamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
147+
) {
145148
if self.channel.eventLoop.inEventLoop {
146-
self.executeRequest0(request)
149+
self.executeRequest0(
150+
request,
151+
streamChannelDebugInitializer: streamChannelDebugInitializer
152+
)
147153
} else {
148154
self.channel.eventLoop.execute {
149-
self.executeRequest0(request)
155+
self.executeRequest0(
156+
request,
157+
streamChannelDebugInitializer: streamChannelDebugInitializer
158+
)
150159
}
151160
}
152161
}
@@ -218,7 +227,10 @@ final class HTTP2Connection {
218227
return readyToAcceptConnectionsPromise.futureResult
219228
}
220229

221-
private func executeRequest0(_ request: HTTPExecutableRequest) {
230+
private func executeRequest0(
231+
_ request: HTTPExecutableRequest,
232+
streamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)?
233+
) {
222234
self.channel.eventLoop.assertInEventLoop()
223235

224236
switch self.state {
@@ -259,8 +271,14 @@ final class HTTP2Connection {
259271
self.openStreams.remove(box)
260272
}
261273

262-
channel.write(request, promise: nil)
263-
return channel.eventLoop.makeSucceededVoidFuture()
274+
if let streamChannelDebugInitializer {
275+
return streamChannelDebugInitializer(channel).map { _ in
276+
channel.write(request, promise: nil)
277+
}
278+
} else {
279+
channel.write(request, promise: nil)
280+
return channel.eventLoop.makeSucceededVoidFuture()
281+
}
264282
} catch {
265283
return channel.eventLoop.makeFailedFuture(error)
266284
}

‎Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift

+38-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,21 @@ extension HTTPConnectionPool.ConnectionFactory {
8484
decompression: self.clientConfiguration.decompression,
8585
logger: logger
8686
)
87-
requester.http1ConnectionCreated(connection)
87+
88+
if let debugInitializer
89+
= self.clientConfiguration.http1_1ConnectionDebugInitializer
90+
{
91+
debugInitializer(channel).whenComplete { debugInitializerResult in
92+
switch debugInitializerResult {
93+
case .success:
94+
requester.http1ConnectionCreated(connection)
95+
case .failure(let error):
96+
requester.failedToCreateHTTPConnection(connectionID, error: error)
97+
}
98+
}
99+
} else {
100+
requester.http1ConnectionCreated(connection)
101+
}
88102
} catch {
89103
requester.failedToCreateHTTPConnection(connectionID, error: error)
90104
}
@@ -99,7 +113,29 @@ extension HTTPConnectionPool.ConnectionFactory {
99113
).whenComplete { result in
100114
switch result {
101115
case .success((let connection, let maximumStreams)):
102-
requester.http2ConnectionCreated(connection, maximumStreams: maximumStreams)
116+
if let debugInitializer
117+
= self.clientConfiguration.http2ConnectionDebugInitializer
118+
{
119+
debugInitializer(channel).whenComplete { debugInitializerResult in
120+
switch debugInitializerResult {
121+
case .success:
122+
requester.http2ConnectionCreated(
123+
connection,
124+
maximumStreams: maximumStreams
125+
)
126+
case .failure(let error):
127+
requester.failedToCreateHTTPConnection(
128+
connectionID,
129+
error: error
130+
)
131+
}
132+
}
133+
} else {
134+
requester.http2ConnectionCreated(
135+
connection,
136+
maximumStreams: maximumStreams
137+
)
138+
}
103139
case .failure(let error):
104140
requester.failedToCreateHTTPConnection(connectionID, error: error)
105141
}

‎Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool.swift

+20-4
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,20 @@ final class HTTPConnectionPool:
321321
private func runUnlockedRequestAction(_ action: Actions.RequestAction.Unlocked) {
322322
switch action {
323323
case .executeRequest(let request, let connection):
324-
connection.executeRequest(request.req)
324+
connection.executeRequest(
325+
request.req,
326+
http2StreamChannelDebugInitializer:
327+
self.clientConfiguration.http2StreamChannelDebugInitializer
328+
)
325329

326330
case .executeRequests(let requests, let connection):
327-
for request in requests { connection.executeRequest(request.req) }
331+
for request in requests {
332+
connection.executeRequest(
333+
request.req,
334+
http2StreamChannelDebugInitializer:
335+
self.clientConfiguration.http2StreamChannelDebugInitializer
336+
)
337+
}
328338

329339
case .failRequest(let request, let error):
330340
request.req.fail(error)
@@ -651,12 +661,18 @@ extension HTTPConnectionPool {
651661
}
652662
}
653663

654-
fileprivate func executeRequest(_ request: HTTPExecutableRequest) {
664+
fileprivate func executeRequest(
665+
_ request: HTTPExecutableRequest,
666+
http2StreamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)?
667+
) {
655668
switch self._ref {
656669
case .http1_1(let connection):
657670
return connection.executeRequest(request)
658671
case .http2(let connection):
659-
return connection.executeRequest(request)
672+
return connection.executeRequest(
673+
request,
674+
streamChannelDebugInitializer: http2StreamChannelDebugInitializer
675+
)
660676
case .__testOnly_connection:
661677
break
662678
}

‎Sources/AsyncHTTPClient/HTTPClient.swift

+66-9
Original file line numberDiff line numberDiff line change
@@ -847,14 +847,32 @@ public class HTTPClient {
847847
/// By default, don't use it
848848
public var enableMultipath: Bool
849849

850+
/// A method with access to the HTTP/1 connection channel that is called when creating the connection.
851+
public var http1_1ConnectionDebugInitializer:
852+
(@Sendable (Channel) -> EventLoopFuture<Void>)?
853+
854+
/// A method with access to the HTTP/2 connection channel that is called when creating the connection.
855+
public var http2ConnectionDebugInitializer:
856+
(@Sendable (Channel) -> EventLoopFuture<Void>)?
857+
858+
/// A method with access to the HTTP/2 stream channel that is called when creating the stream.
859+
public var http2StreamChannelDebugInitializer:
860+
(@Sendable (Channel) -> EventLoopFuture<Void>)?
861+
850862
public init(
851863
tlsConfiguration: TLSConfiguration? = nil,
852864
redirectConfiguration: RedirectConfiguration? = nil,
853865
timeout: Timeout = Timeout(),
854866
connectionPool: ConnectionPool = ConnectionPool(),
855867
proxy: Proxy? = nil,
856868
ignoreUncleanSSLShutdown: Bool = false,
857-
decompression: Decompression = .disabled
869+
decompression: Decompression = .disabled,
870+
http1_1ConnectionDebugInitializer:
871+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
872+
http2ConnectionDebugInitializer:
873+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
874+
http2StreamChannelDebugInitializer:
875+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
858876
) {
859877
self.tlsConfiguration = tlsConfiguration
860878
self.redirectConfiguration = redirectConfiguration ?? RedirectConfiguration()
@@ -865,6 +883,9 @@ public class HTTPClient {
865883
self.httpVersion = .automatic
866884
self.networkFrameworkWaitForConnectivity = true
867885
self.enableMultipath = false
886+
self.http1_1ConnectionDebugInitializer = http1_1ConnectionDebugInitializer
887+
self.http2ConnectionDebugInitializer = http2ConnectionDebugInitializer
888+
self.http2StreamChannelDebugInitializer = http2StreamChannelDebugInitializer
868889
}
869890

870891
public init(
@@ -873,7 +894,13 @@ public class HTTPClient {
873894
timeout: Timeout = Timeout(),
874895
proxy: Proxy? = nil,
875896
ignoreUncleanSSLShutdown: Bool = false,
876-
decompression: Decompression = .disabled
897+
decompression: Decompression = .disabled,
898+
http1_1ConnectionDebugInitializer:
899+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
900+
http2ConnectionDebugInitializer:
901+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
902+
http2StreamChannelDebugInitializer:
903+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
877904
) {
878905
self.init(
879906
tlsConfiguration: tlsConfiguration,
@@ -882,7 +909,10 @@ public class HTTPClient {
882909
connectionPool: ConnectionPool(),
883910
proxy: proxy,
884911
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
885-
decompression: decompression
912+
decompression: decompression,
913+
http1_1ConnectionDebugInitializer: http1_1ConnectionDebugInitializer,
914+
http2ConnectionDebugInitializer: http2ConnectionDebugInitializer,
915+
http2StreamChannelDebugInitializer: http2StreamChannelDebugInitializer
886916
)
887917
}
888918

@@ -893,7 +923,13 @@ public class HTTPClient {
893923
maximumAllowedIdleTimeInConnectionPool: TimeAmount = .seconds(60),
894924
proxy: Proxy? = nil,
895925
ignoreUncleanSSLShutdown: Bool = false,
896-
decompression: Decompression = .disabled
926+
decompression: Decompression = .disabled,
927+
http1_1ConnectionDebugInitializer:
928+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
929+
http2ConnectionDebugInitializer:
930+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
931+
http2StreamChannelDebugInitializer:
932+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
897933
) {
898934
var tlsConfig = TLSConfiguration.makeClientConfiguration()
899935
tlsConfig.certificateVerification = certificateVerification
@@ -904,7 +940,10 @@ public class HTTPClient {
904940
connectionPool: ConnectionPool(idleTimeout: maximumAllowedIdleTimeInConnectionPool),
905941
proxy: proxy,
906942
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
907-
decompression: decompression
943+
decompression: decompression,
944+
http1_1ConnectionDebugInitializer: http1_1ConnectionDebugInitializer,
945+
http2ConnectionDebugInitializer: http2ConnectionDebugInitializer,
946+
http2StreamChannelDebugInitializer: http2StreamChannelDebugInitializer
908947
)
909948
}
910949

@@ -916,7 +955,13 @@ public class HTTPClient {
916955
proxy: Proxy? = nil,
917956
ignoreUncleanSSLShutdown: Bool = false,
918957
decompression: Decompression = .disabled,
919-
backgroundActivityLogger: Logger?
958+
backgroundActivityLogger: Logger?,
959+
http1_1ConnectionDebugInitializer:
960+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
961+
http2ConnectionDebugInitializer:
962+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
963+
http2StreamChannelDebugInitializer:
964+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
920965
) {
921966
var tlsConfig = TLSConfiguration.makeClientConfiguration()
922967
tlsConfig.certificateVerification = certificateVerification
@@ -927,7 +972,10 @@ public class HTTPClient {
927972
connectionPool: ConnectionPool(idleTimeout: connectionPool),
928973
proxy: proxy,
929974
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
930-
decompression: decompression
975+
decompression: decompression,
976+
http1_1ConnectionDebugInitializer: http1_1ConnectionDebugInitializer,
977+
http2ConnectionDebugInitializer: http2ConnectionDebugInitializer,
978+
http2StreamChannelDebugInitializer: http2StreamChannelDebugInitializer
931979
)
932980
}
933981

@@ -937,7 +985,13 @@ public class HTTPClient {
937985
timeout: Timeout = Timeout(),
938986
proxy: Proxy? = nil,
939987
ignoreUncleanSSLShutdown: Bool = false,
940-
decompression: Decompression = .disabled
988+
decompression: Decompression = .disabled,
989+
http1_1ConnectionDebugInitializer:
990+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
991+
http2ConnectionDebugInitializer:
992+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
993+
http2StreamChannelDebugInitializer:
994+
(@Sendable (Channel) -> EventLoopFuture<Void>)? = nil
941995
) {
942996
self.init(
943997
certificateVerification: certificateVerification,
@@ -946,7 +1000,10 @@ public class HTTPClient {
9461000
maximumAllowedIdleTimeInConnectionPool: .seconds(60),
9471001
proxy: proxy,
9481002
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
949-
decompression: decompression
1003+
decompression: decompression,
1004+
http1_1ConnectionDebugInitializer: http1_1ConnectionDebugInitializer,
1005+
http2ConnectionDebugInitializer: http2ConnectionDebugInitializer,
1006+
http2StreamChannelDebugInitializer: http2StreamChannelDebugInitializer
9501007
)
9511008
}
9521009
}

‎Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+85
Original file line numberDiff line numberDiff line change
@@ -4306,4 +4306,89 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass {
43064306
request.setBasicAuth(username: "foo", password: "bar")
43074307
XCTAssertEqual(request.headers.first(name: "Authorization"), "Basic Zm9vOmJhcg==")
43084308
}
4309+
4310+
func testHTTP1ConnectionDebugInitializer() {
4311+
let connectionDebugInitializerUtil = DebugInitializerUtil()
4312+
4313+
var config = HTTPClient.Configuration()
4314+
config.tlsConfiguration = .clientDefault
4315+
config.tlsConfiguration?.certificateVerification = .none
4316+
config.httpVersion = .http1Only
4317+
config.http1_1ConnectionDebugInitializer = connectionDebugInitializerUtil.operation
4318+
4319+
let client = HTTPClient(
4320+
eventLoopGroupProvider: .singleton,
4321+
configuration: config,
4322+
backgroundActivityLogger: Logger(
4323+
label: "HTTPClient",
4324+
factory: StreamLogHandler.standardOutput(label:)
4325+
)
4326+
)
4327+
defer { XCTAssertNoThrow(client.shutdown()) }
4328+
4329+
let bin = HTTPBin(.http1_1(ssl: true, compress: false))
4330+
defer { XCTAssertNoThrow(try bin.shutdown()) }
4331+
4332+
for _ in 0..<3 {
4333+
XCTAssertNoThrow(try client.get(url: "https://localhost:\(bin.port)/get").wait())
4334+
}
4335+
4336+
// Even though multiple requests were made, the connection debug initializer must be called
4337+
// only once.
4338+
XCTAssertEqual(connectionDebugInitializerUtil.executionCount, 1)
4339+
}
4340+
4341+
func testHTTP2ConnectionAndStreamChannelDebugInitializers() {
4342+
let connectionDebugInitializerUtil = DebugInitializerUtil()
4343+
let streamChannelDebugInitializerUtil = DebugInitializerUtil()
4344+
4345+
var config = HTTPClient.Configuration()
4346+
config.tlsConfiguration = .clientDefault
4347+
config.tlsConfiguration?.certificateVerification = .none
4348+
config.httpVersion = .automatic
4349+
config.http2ConnectionDebugInitializer = connectionDebugInitializerUtil.operation
4350+
config.http2StreamChannelDebugInitializer = streamChannelDebugInitializerUtil.operation
4351+
4352+
let client = HTTPClient(
4353+
eventLoopGroupProvider: .singleton,
4354+
configuration: config,
4355+
backgroundActivityLogger: Logger(
4356+
label: "HTTPClient",
4357+
factory: StreamLogHandler.standardOutput(label:)
4358+
)
4359+
)
4360+
defer { XCTAssertNoThrow(client.shutdown()) }
4361+
4362+
let bin = HTTPBin(.http2(compress: false))
4363+
defer { XCTAssertNoThrow(try bin.shutdown()) }
4364+
4365+
let numberOfRequests = 3
4366+
4367+
for _ in 0..<numberOfRequests {
4368+
XCTAssertNoThrow(try client.get(url: "https://localhost:\(bin.port)/get").wait())
4369+
}
4370+
4371+
// Even though multiple requests were made, the connection debug initializer must be called
4372+
// only once.
4373+
XCTAssertEqual(connectionDebugInitializerUtil.executionCount, 1)
4374+
4375+
// The stream channel debug initializer must be called only as much as the number of
4376+
// requests made.
4377+
XCTAssertEqual(streamChannelDebugInitializerUtil.executionCount, numberOfRequests)
4378+
}
4379+
}
4380+
4381+
class DebugInitializerUtil {
4382+
var executionCount: Int
4383+
4384+
@Sendable
4385+
func operation(channel: Channel) -> EventLoopFuture<Void> {
4386+
self.executionCount += 1
4387+
4388+
return channel.eventLoop.makeSucceededVoidFuture()
4389+
}
4390+
4391+
init() {
4392+
self.executionCount = 0
4393+
}
43094394
}

0 commit comments

Comments
 (0)
Please sign in to comment.