mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-06-02 07:37:34 +00:00
HTTPClient.shared a globally shared singleton & .browserLike configuration (#705)
Co-authored-by: Johannes Weiss <johannes@jweiss.io>
This commit is contained in:
co-authored by
Johannes Weiss
parent
36292f9d57
commit
e0977cf290
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
Reference in New Issue
Block a user