Skip to content

Commit 42b1218

Browse files
author
Johannes Weiss
committed
HTTPClient.shared a globally shared singleton & .browserLike configuration
1 parent 2ff11f5 commit 42b1218

File tree

4 files changed

+85
-3
lines changed

4 files changed

+85
-3
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
extension HTTPClient.Configuration {
2+
/// A ``HTTPClient/Configuration`` that tries to mimick the platform's default or prevalent browser as closely as possible.
3+
///
4+
/// Platform's default/prevalent browsers that we're trying to match (these might change over time):
5+
/// - macOS: Safari
6+
/// - iOS: Safari
7+
/// - Android: Google Chrome
8+
/// - Linux (non-Android): Google Chrome
9+
///
10+
/// - note: Don't rely on the values in here never changing, they will be adapted to better match the platform's default/prevalent browser.
11+
public static var browserLike: HTTPClient.Configuration {
12+
// To start with, let's go with these values. Obtained from Firefox's config.
13+
return HTTPClient.Configuration(
14+
certificateVerification: .fullVerification,
15+
redirectConfiguration: .follow(max: 20, allowCycles: false),
16+
timeout: Timeout(connect: .seconds(90), read: .seconds(90)),
17+
connectionPool: .seconds(600),
18+
proxy: nil,
19+
ignoreUncleanSSLShutdown: false,
20+
decompression: .enabled(limit: .ratio(10)),
21+
backgroundActivityLogger: nil
22+
)
23+
}
24+
}

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public class HTTPClient {
7878

7979
private var state: State
8080
private let stateLock = NIOLock()
81+
private let canBeShutDown: Bool
8182

8283
internal static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() })
8384

@@ -133,9 +134,20 @@ public class HTTPClient {
133134
/// - eventLoopGroup: The `EventLoopGroup` that the ``HTTPClient`` will use.
134135
/// - configuration: Client configuration.
135136
/// - backgroundActivityLogger: The `Logger` that will be used to log background any activity that's not associated with a request.
136-
public required init(eventLoopGroup: any EventLoopGroup,
137-
configuration: Configuration = Configuration(),
138-
backgroundActivityLogger: Logger) {
137+
public convenience init(eventLoopGroup: any EventLoopGroup,
138+
configuration: Configuration = Configuration(),
139+
backgroundActivityLogger: Logger) {
140+
self.init(eventLoopGroup: eventLoopGroup,
141+
configuration: configuration,
142+
backgroundActivityLogger: backgroundActivityLogger,
143+
canBeShutDown: true)
144+
}
145+
146+
internal required init(eventLoopGroup: EventLoopGroup,
147+
configuration: Configuration = Configuration(),
148+
backgroundActivityLogger: Logger,
149+
canBeShutDown: Bool) {
150+
self.canBeShutDown = canBeShutDown
139151
self.eventLoopGroup = eventLoopGroup
140152
self.configuration = configuration
141153
self.poolManager = HTTPConnectionPool.Manager(
@@ -247,6 +259,12 @@ public class HTTPClient {
247259
}
248260

249261
private func shutdown(requiresCleanClose: Bool, queue: DispatchQueue, _ callback: @escaping ShutdownCallback) {
262+
guard self.canBeShutDown else {
263+
queue.async {
264+
callback(HTTPClientError.shutdownUnsupported)
265+
}
266+
return
267+
}
250268
do {
251269
try self.stateLock.withLock {
252270
guard case .upAndRunning = self.state else {
@@ -1069,6 +1087,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
10691087
case getConnectionFromPoolTimeout
10701088
case deadlineExceeded
10711089
case httpEndReceivedAfterHeadWith1xx
1090+
case shutdownUnsupported
10721091
}
10731092

10741093
private var code: Code
@@ -1150,6 +1169,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
11501169
return "Deadline exceeded"
11511170
case .httpEndReceivedAfterHeadWith1xx:
11521171
return "HTTP end received after head with 1xx"
1172+
case .shutdownUnsupported:
1173+
return "The global singleton HTTP client cannot be shut down"
11531174
}
11541175
}
11551176

@@ -1214,6 +1235,11 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
12141235
return HTTPClientError(code: .serverOfferedUnsupportedApplicationProtocol(proto))
12151236
}
12161237

1238+
/// The globally shared singleton ``HTTPClient`` cannot be shut down.
1239+
public static var shutdownUnsupported: HTTPClientError {
1240+
return HTTPClientError(code: .shutdownUnsupported)
1241+
}
1242+
12171243
/// The request deadline was exceeded. The request was cancelled because of this.
12181244
public static let deadlineExceeded = HTTPClientError(code: .deadlineExceeded)
12191245

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
extension HTTPClient {
2+
/// A globally shared, singleton ``HTTPClient``.
3+
///
4+
/// The returned client uses the following settings:
5+
/// - configuration is ``HTTPClient/Configuration/browserLike`` (matching the platform's default/prevalent browser as well as possible)
6+
/// - `EventLoopGroup` is ``HTTPClient/defaultEventLoopGroup`` (matching the platform default)
7+
/// - logging is disabled
8+
public static var shared: HTTPClient {
9+
return globallySharedHTTPClient
10+
}
11+
}
12+
13+
private let globallySharedHTTPClient: HTTPClient = {
14+
let httpClient = HTTPClient(
15+
eventLoopGroup: HTTPClient.defaultEventLoopGroup,
16+
configuration: .browserLike,
17+
backgroundActivityLogger: HTTPClient.loggingDisabled,
18+
canBeShutDown: false
19+
)
20+
return httpClient
21+
}()

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3525,4 +3525,15 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass {
35253525
let response = try client.get(url: self.defaultHTTPBinURLPrefix + "get").wait()
35263526
XCTAssertEqual(.ok, response.status)
35273527
}
3528+
3529+
func testSingletonClientWorks() throws {
3530+
let response = try HTTPClient.shared.get(url: self.defaultHTTPBinURLPrefix + "get").wait()
3531+
XCTAssertEqual(.ok, response.status)
3532+
}
3533+
3534+
func testSingletonClientCannotBeShutDown() {
3535+
XCTAssertThrowsError(try HTTPClient.shared.shutdown().wait()) { error in
3536+
XCTAssertEqual(.shutdownUnsupported, error as? HTTPClientError)
3537+
}
3538+
}
35283539
}

0 commit comments

Comments
 (0)