HTTPClient.shared a globally shared singleton & .browserLike configuration (#705)

Co-authored-by: Johannes Weiss <johannes@jweiss.io>
This commit is contained in:
Johannes Weiss
2024-04-03 05:54:00 -07:00
committed by GitHub
co-authored by Johannes Weiss
parent 36292f9d57
commit e0977cf290
6 changed files with 138 additions and 92 deletions
@@ -0,0 +1,41 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
extension HTTPClient.Configuration {
/// The ``HTTPClient/Configuration`` for ``HTTPClient/shared`` which tries to mimic the platform's default or prevalent browser as closely as possible.
///
/// Don't rely on specific values of this configuration as they're subject to change. You can rely on them being somewhat sensible though.
///
/// - note: At present, this configuration is nowhere close to a real browser configuration but in case of disagreements we will choose values that match
/// the default browser as closely as possible.
///
/// Platform's default/prevalent browsers that we're trying to match (these might change over time):
/// - macOS: Safari
/// - iOS: Safari
/// - Android: Google Chrome
/// - Linux (non-Android): Google Chrome
public static var singletonConfiguration: HTTPClient.Configuration {
// To start with, let's go with these values. Obtained from Firefox's config.
return HTTPClient.Configuration(
certificateVerification: .fullVerification,
redirectConfiguration: .follow(max: 20, allowCycles: false),
timeout: Timeout(connect: .seconds(90), read: .seconds(90)),
connectionPool: .seconds(600),
proxy: nil,
ignoreUncleanSSLShutdown: false,
decompression: .enabled(limit: .ratio(10)),
backgroundActivityLogger: nil
)
}
}
+7 -53
View File
@@ -34,12 +34,6 @@ The code snippet below illustrates how to make a simple GET request to a remote
```swift
import AsyncHTTPClient
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
defer {
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
try! httpClient.syncShutdown()
}
/// MARK: - Using Swift Concurrency
let request = HTTPClientRequest(url: "https://apple.com/")
let response = try await httpClient.execute(request, timeout: .seconds(30))
@@ -53,7 +47,7 @@ if response.status == .ok {
/// MARK: - Using SwiftNIO EventLoopFuture
httpClient.get(url: "https://apple.com/").whenComplete { result in
HTTPClient.shared.get(url: "https://apple.com/").whenComplete { result in
switch result {
case .failure(let error):
// process error
@@ -82,19 +76,13 @@ The default HTTP Method is `GET`. In case you need to have more control over the
```swift
import AsyncHTTPClient
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
defer {
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
try! httpClient.syncShutdown()
}
do {
var request = HTTPClientRequest(url: "https://apple.com/")
request.method = .POST
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
request.body = .bytes(ByteBuffer(string: "some data"))
let response = try await httpClient.execute(request, timeout: .seconds(30))
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
if response.status == .ok {
// handle response
} else {
@@ -103,8 +91,6 @@ do {
} catch {
// handle error
}
// it's important to shutdown the httpClient after all requests are done, even if one failed
try await httpClient.shutdown()
```
#### Using SwiftNIO EventLoopFuture
@@ -112,17 +98,11 @@ try await httpClient.shutdown()
```swift
import AsyncHTTPClient
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
defer {
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
try! httpClient.syncShutdown()
}
var request = try HTTPClient.Request(url: "https://apple.com/", method: .POST)
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
request.body = .string("some-body")
httpClient.execute(request: request).whenComplete { result in
HTTPClient.shared.execute(request: request).whenComplete { result in
switch result {
case .failure(let error):
// process error
@@ -161,15 +141,9 @@ The following example demonstrates how to count the number of bytes in a streami
##### Using Swift Concurrency
```swift
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
defer {
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
try! httpClient.syncShutdown()
}
do {
let request = HTTPClientRequest(url: "https://apple.com/")
let response = try await httpClient.execute(request, timeout: .seconds(30))
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
print("HTTP head", response)
// if defined, the content-length headers announces the size of the body
@@ -192,8 +166,6 @@ do {
} catch {
print("request failed:", error)
}
// it is important to shutdown the httpClient after all requests are done, even if one failed
try await httpClient.shutdown()
```
##### Using HTTPClientResponseDelegate and SwiftNIO EventLoopFuture
@@ -266,12 +238,6 @@ asynchronously, while reporting the download progress at the same time, like in
example:
```swift
let client = HTTPClient(eventLoopGroupProvider: .singleton)
defer {
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
try! httpClient.syncShutdown()
}
let request = try HTTPClient.Request(
url: "https://swift.org/builds/development/ubuntu1804/latest-build.yml"
)
@@ -283,7 +249,7 @@ let delegate = try FileDownloadDelegate(path: "/tmp/latest-build.yml", reportPro
print("Downloaded \($0.receivedBytes) bytes so far")
})
client.execute(request: request, delegate: delegate).futureResult
HTTPClient.shared.execute(request: request, delegate: delegate).futureResult
.whenSuccess { progress in
if let totalBytes = progress.totalBytes {
print("Final total bytes count: \(totalBytes)")
@@ -295,13 +261,7 @@ client.execute(request: request, delegate: delegate).futureResult
#### Unix Domain Socket Paths
Connecting to servers bound to socket paths is easy:
```swift
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
defer {
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
try! httpClient.syncShutdown()
}
httpClient.execute(
HTTPClient.shared.execute(
.GET,
socketPath: "/tmp/myServer.socket",
urlPath: "/path/to/resource"
@@ -310,13 +270,7 @@ httpClient.execute(
Connecting over TLS to a unix domain socket path is possible as well:
```swift
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
defer {
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
try! httpClient.syncShutdown()
}
httpClient.execute(
HTTPClient.shared.execute(
.POST,
secureSocketPath: "/tmp/myServer.socket",
urlPath: "/path/to/resource",
+30 -11
View File
@@ -44,8 +44,7 @@ let globalRequestID = ManagedAtomic(0)
/// Example:
///
/// ```swift
/// let client = HTTPClient(eventLoopGroupProvider: .singleton)
/// client.get(url: "https://swift.org", deadline: .now() + .seconds(1)).whenComplete { result in
/// HTTPClient.shared.get(url: "https://swift.org", deadline: .now() + .seconds(1)).whenComplete { result in
/// switch result {
/// case .failure(let error):
/// // process error
@@ -58,12 +57,6 @@ let globalRequestID = ManagedAtomic(0)
/// }
/// }
/// ```
///
/// It is important to close the client instance, for example in a defer statement, after use to cleanly shutdown the underlying NIO `EventLoopGroup`:
///
/// ```swift
/// try client.syncShutdown()
/// ```
public class HTTPClient {
/// The `EventLoopGroup` in use by this ``HTTPClient``.
///
@@ -78,6 +71,7 @@ public class HTTPClient {
private var state: State
private let stateLock = NIOLock()
private let canBeShutDown: Bool
static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() })
@@ -133,9 +127,20 @@ public class HTTPClient {
/// - eventLoopGroup: The `EventLoopGroup` that the ``HTTPClient`` will use.
/// - configuration: Client configuration.
/// - backgroundActivityLogger: The `Logger` that will be used to log background any activity that's not associated with a request.
public required init(eventLoopGroup: any EventLoopGroup = HTTPClient.defaultEventLoopGroup,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger) {
public convenience init(eventLoopGroup: any EventLoopGroup = HTTPClient.defaultEventLoopGroup,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger) {
self.init(eventLoopGroup: eventLoopGroup,
configuration: configuration,
backgroundActivityLogger: backgroundActivityLogger,
canBeShutDown: true)
}
internal required init(eventLoopGroup: EventLoopGroup,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger,
canBeShutDown: Bool) {
self.canBeShutDown = canBeShutDown
self.eventLoopGroup = eventLoopGroup
self.configuration = configuration
self.poolManager = HTTPConnectionPool.Manager(
@@ -238,6 +243,12 @@ public class HTTPClient {
}
private func shutdown(requiresCleanClose: Bool, queue: DispatchQueue, _ callback: @escaping ShutdownCallback) {
guard self.canBeShutDown else {
queue.async {
callback(HTTPClientError.shutdownUnsupported)
}
return
}
do {
try self.stateLock.withLock {
guard case .upAndRunning = self.state else {
@@ -1081,6 +1092,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
case getConnectionFromPoolTimeout
case deadlineExceeded
case httpEndReceivedAfterHeadWith1xx
case shutdownUnsupported
}
private var code: Code
@@ -1164,6 +1176,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
return "Deadline exceeded"
case .httpEndReceivedAfterHeadWith1xx:
return "HTTP end received after head with 1xx"
case .shutdownUnsupported:
return "The global singleton HTTP client cannot be shut down"
}
}
@@ -1230,6 +1244,11 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
return HTTPClientError(code: .serverOfferedUnsupportedApplicationProtocol(proto))
}
/// The globally shared singleton ``HTTPClient`` cannot be shut down.
public static var shutdownUnsupported: HTTPClientError {
return HTTPClientError(code: .shutdownUnsupported)
}
/// The request deadline was exceeded. The request was cancelled because of this.
public static let deadlineExceeded = HTTPClientError(code: .deadlineExceeded)
+35
View File
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
extension HTTPClient {
/// A globally shared, singleton ``HTTPClient``.
///
/// The returned client uses the following settings:
/// - configuration is ``HTTPClient/Configuration/singletonConfiguration`` (matching the platform's default/prevalent browser as well as possible)
/// - `EventLoopGroup` is ``HTTPClient/defaultEventLoopGroup`` (matching the platform default)
/// - logging is disabled
public static var shared: HTTPClient {
return globallySharedHTTPClient
}
}
private let globallySharedHTTPClient: HTTPClient = {
let httpClient = HTTPClient(
eventLoopGroup: HTTPClient.defaultEventLoopGroup,
configuration: .singletonConfiguration,
backgroundActivityLogger: HTTPClient.loggingDisabled,
canBeShutDown: false
)
return httpClient
}()