Skip to content

Commit e0977cf

Browse files
weissiJohannes Weiss
and
Johannes Weiss
authored
HTTPClient.shared a globally shared singleton & .browserLike configuration (swift-server#705)
Co-authored-by: Johannes Weiss <[email protected]>
1 parent 36292f9 commit e0977cf

File tree

6 files changed

+138
-92
lines changed

6 files changed

+138
-92
lines changed

README.md

+14-28
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,9 @@ The code snippet below illustrates how to make a simple GET request to a remote
3030
```swift
3131
import AsyncHTTPClient
3232

33-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
34-
3533
/// MARK: - Using Swift Concurrency
3634
let request = HTTPClientRequest(url: "https://apple.com/")
37-
let response = try await httpClient.execute(request, timeout: .seconds(30))
35+
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
3836
print("HTTP head", response)
3937
if response.status == .ok {
4038
let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
@@ -45,7 +43,7 @@ if response.status == .ok {
4543

4644

4745
/// MARK: - Using SwiftNIO EventLoopFuture
48-
httpClient.get(url: "https://apple.com/").whenComplete { result in
46+
HTTPClient.shared.get(url: "https://apple.com/").whenComplete { result in
4947
switch result {
5048
case .failure(let error):
5149
// process error
@@ -59,7 +57,8 @@ httpClient.get(url: "https://apple.com/").whenComplete { result in
5957
}
6058
```
6159

62-
You should always shut down `HTTPClient` instances you created using `try httpClient.shutdown()`. Please note that you must not call `httpClient.shutdown` before all requests of the HTTP client have finished, or else the in-flight requests will likely fail because their network connections are interrupted.
60+
If you create your own `HTTPClient` instances, you should shut them down using `httpClient.shutdown()` when you're done using them. Failing to do so will leak resources.
61+
Please note that you must not call `httpClient.shutdown` before all requests of the HTTP client have finished, or else the in-flight requests will likely fail because their network connections are interrupted.
6362

6463
### async/await examples
6564

@@ -74,14 +73,13 @@ The default HTTP Method is `GET`. In case you need to have more control over the
7473
```swift
7574
import AsyncHTTPClient
7675

77-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
7876
do {
7977
var request = HTTPClientRequest(url: "https://apple.com/")
8078
request.method = .POST
8179
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
8280
request.body = .bytes(ByteBuffer(string: "some data"))
8381

84-
let response = try await httpClient.execute(request, timeout: .seconds(30))
82+
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
8583
if response.status == .ok {
8684
// handle response
8785
} else {
@@ -90,26 +88,18 @@ do {
9088
} catch {
9189
// handle error
9290
}
93-
// it's important to shutdown the httpClient after all requests are done, even if one failed
94-
try await httpClient.shutdown()
9591
```
9692

9793
#### Using SwiftNIO EventLoopFuture
9894

9995
```swift
10096
import AsyncHTTPClient
10197

102-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
103-
defer {
104-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
105-
try! httpClient.syncShutdown()
106-
}
107-
10898
var request = try HTTPClient.Request(url: "https://apple.com/", method: .POST)
10999
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
110100
request.body = .string("some-body")
111101

112-
httpClient.execute(request: request).whenComplete { result in
102+
HTTPClient.shared.execute(request: request).whenComplete { result in
113103
switch result {
114104
case .failure(let error):
115105
// process error
@@ -124,7 +114,9 @@ httpClient.execute(request: request).whenComplete { result in
124114
```
125115

126116
### Redirects following
127-
Enable follow-redirects behavior using the client configuration:
117+
118+
The globally shared instance `HTTPClient.shared` follows redirects by default. If you create your own `HTTPClient`, you can enable the follow-redirects behavior using the client configuration:
119+
128120
```swift
129121
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton,
130122
configuration: HTTPClient.Configuration(followRedirects: true))
@@ -148,10 +140,9 @@ The following example demonstrates how to count the number of bytes in a streami
148140

149141
#### Using Swift Concurrency
150142
```swift
151-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
152143
do {
153144
let request = HTTPClientRequest(url: "https://apple.com/")
154-
let response = try await httpClient.execute(request, timeout: .seconds(30))
145+
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
155146
print("HTTP head", response)
156147

157148
// if defined, the content-length headers announces the size of the body
@@ -174,8 +165,6 @@ do {
174165
} catch {
175166
print("request failed:", error)
176167
}
177-
// it is important to shutdown the httpClient after all requests are done, even if one failed
178-
try await httpClient.shutdown()
179168
```
180169

181170
#### Using HTTPClientResponseDelegate and SwiftNIO EventLoopFuture
@@ -235,7 +224,7 @@ class CountingDelegate: HTTPClientResponseDelegate {
235224
let request = try HTTPClient.Request(url: "https://apple.com/")
236225
let delegate = CountingDelegate()
237226

238-
httpClient.execute(request: request, delegate: delegate).futureResult.whenSuccess { count in
227+
HTTPClient.shared.execute(request: request, delegate: delegate).futureResult.whenSuccess { count in
239228
print(count)
240229
}
241230
```
@@ -248,7 +237,6 @@ asynchronously, while reporting the download progress at the same time, like in
248237
example:
249238

250239
```swift
251-
let client = HTTPClient(eventLoopGroupProvider: .singleton)
252240
let request = try HTTPClient.Request(
253241
url: "https://swift.org/builds/development/ubuntu1804/latest-build.yml"
254242
)
@@ -260,7 +248,7 @@ let delegate = try FileDownloadDelegate(path: "/tmp/latest-build.yml", reportPro
260248
print("Downloaded \($0.receivedBytes) bytes so far")
261249
})
262250

263-
client.execute(request: request, delegate: delegate).futureResult
251+
HTTPClient.shared.execute(request: request, delegate: delegate).futureResult
264252
.whenSuccess { progress in
265253
if let totalBytes = progress.totalBytes {
266254
print("Final total bytes count: \(totalBytes)")
@@ -272,8 +260,7 @@ client.execute(request: request, delegate: delegate).futureResult
272260
### Unix Domain Socket Paths
273261
Connecting to servers bound to socket paths is easy:
274262
```swift
275-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
276-
httpClient.execute(
263+
HTTPClient.shared.execute(
277264
.GET,
278265
socketPath: "/tmp/myServer.socket",
279266
urlPath: "/path/to/resource"
@@ -282,8 +269,7 @@ httpClient.execute(
282269

283270
Connecting over TLS to a unix domain socket path is possible as well:
284271
```swift
285-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
286-
httpClient.execute(
272+
HTTPClient.shared.execute(
287273
.POST,
288274
secureSocketPath: "/tmp/myServer.socket",
289275
urlPath: "/path/to/resource",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
extension HTTPClient.Configuration {
16+
/// The ``HTTPClient/Configuration`` for ``HTTPClient/shared`` which tries to mimic the platform's default or prevalent browser as closely as possible.
17+
///
18+
/// Don't rely on specific values of this configuration as they're subject to change. You can rely on them being somewhat sensible though.
19+
///
20+
/// - note: At present, this configuration is nowhere close to a real browser configuration but in case of disagreements we will choose values that match
21+
/// the default browser as closely as possible.
22+
///
23+
/// Platform's default/prevalent browsers that we're trying to match (these might change over time):
24+
/// - macOS: Safari
25+
/// - iOS: Safari
26+
/// - Android: Google Chrome
27+
/// - Linux (non-Android): Google Chrome
28+
public static var singletonConfiguration: HTTPClient.Configuration {
29+
// To start with, let's go with these values. Obtained from Firefox's config.
30+
return HTTPClient.Configuration(
31+
certificateVerification: .fullVerification,
32+
redirectConfiguration: .follow(max: 20, allowCycles: false),
33+
timeout: Timeout(connect: .seconds(90), read: .seconds(90)),
34+
connectionPool: .seconds(600),
35+
proxy: nil,
36+
ignoreUncleanSSLShutdown: false,
37+
decompression: .enabled(limit: .ratio(10)),
38+
backgroundActivityLogger: nil
39+
)
40+
}
41+
}

Sources/AsyncHTTPClient/Docs.docc/index.md

+7-53
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,6 @@ The code snippet below illustrates how to make a simple GET request to a remote
3434
```swift
3535
import AsyncHTTPClient
3636

37-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
38-
defer {
39-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
40-
try! httpClient.syncShutdown()
41-
}
42-
4337
/// MARK: - Using Swift Concurrency
4438
let request = HTTPClientRequest(url: "https://apple.com/")
4539
let response = try await httpClient.execute(request, timeout: .seconds(30))
@@ -53,7 +47,7 @@ if response.status == .ok {
5347

5448

5549
/// MARK: - Using SwiftNIO EventLoopFuture
56-
httpClient.get(url: "https://apple.com/").whenComplete { result in
50+
HTTPClient.shared.get(url: "https://apple.com/").whenComplete { result in
5751
switch result {
5852
case .failure(let error):
5953
// process error
@@ -82,19 +76,13 @@ The default HTTP Method is `GET`. In case you need to have more control over the
8276
```swift
8377
import AsyncHTTPClient
8478

85-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
86-
defer {
87-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
88-
try! httpClient.syncShutdown()
89-
}
90-
9179
do {
9280
var request = HTTPClientRequest(url: "https://apple.com/")
9381
request.method = .POST
9482
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
9583
request.body = .bytes(ByteBuffer(string: "some data"))
9684

97-
let response = try await httpClient.execute(request, timeout: .seconds(30))
85+
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
9886
if response.status == .ok {
9987
// handle response
10088
} else {
@@ -103,26 +91,18 @@ do {
10391
} catch {
10492
// handle error
10593
}
106-
// it's important to shutdown the httpClient after all requests are done, even if one failed
107-
try await httpClient.shutdown()
10894
```
10995

11096
#### Using SwiftNIO EventLoopFuture
11197

11298
```swift
11399
import AsyncHTTPClient
114100

115-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
116-
defer {
117-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
118-
try! httpClient.syncShutdown()
119-
}
120-
121101
var request = try HTTPClient.Request(url: "https://apple.com/", method: .POST)
122102
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
123103
request.body = .string("some-body")
124104

125-
httpClient.execute(request: request).whenComplete { result in
105+
HTTPClient.shared.execute(request: request).whenComplete { result in
126106
switch result {
127107
case .failure(let error):
128108
// process error
@@ -161,15 +141,9 @@ The following example demonstrates how to count the number of bytes in a streami
161141

162142
##### Using Swift Concurrency
163143
```swift
164-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
165-
defer {
166-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
167-
try! httpClient.syncShutdown()
168-
}
169-
170144
do {
171145
let request = HTTPClientRequest(url: "https://apple.com/")
172-
let response = try await httpClient.execute(request, timeout: .seconds(30))
146+
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
173147
print("HTTP head", response)
174148

175149
// if defined, the content-length headers announces the size of the body
@@ -192,8 +166,6 @@ do {
192166
} catch {
193167
print("request failed:", error)
194168
}
195-
// it is important to shutdown the httpClient after all requests are done, even if one failed
196-
try await httpClient.shutdown()
197169
```
198170

199171
##### Using HTTPClientResponseDelegate and SwiftNIO EventLoopFuture
@@ -266,12 +238,6 @@ asynchronously, while reporting the download progress at the same time, like in
266238
example:
267239

268240
```swift
269-
let client = HTTPClient(eventLoopGroupProvider: .singleton)
270-
defer {
271-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
272-
try! httpClient.syncShutdown()
273-
}
274-
275241
let request = try HTTPClient.Request(
276242
url: "https://swift.org/builds/development/ubuntu1804/latest-build.yml"
277243
)
@@ -283,7 +249,7 @@ let delegate = try FileDownloadDelegate(path: "/tmp/latest-build.yml", reportPro
283249
print("Downloaded \($0.receivedBytes) bytes so far")
284250
})
285251

286-
client.execute(request: request, delegate: delegate).futureResult
252+
HTTPClient.shared.execute(request: request, delegate: delegate).futureResult
287253
.whenSuccess { progress in
288254
if let totalBytes = progress.totalBytes {
289255
print("Final total bytes count: \(totalBytes)")
@@ -295,13 +261,7 @@ client.execute(request: request, delegate: delegate).futureResult
295261
#### Unix Domain Socket Paths
296262
Connecting to servers bound to socket paths is easy:
297263
```swift
298-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
299-
defer {
300-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
301-
try! httpClient.syncShutdown()
302-
}
303-
304-
httpClient.execute(
264+
HTTPClient.shared.execute(
305265
.GET,
306266
socketPath: "/tmp/myServer.socket",
307267
urlPath: "/path/to/resource"
@@ -310,13 +270,7 @@ httpClient.execute(
310270

311271
Connecting over TLS to a unix domain socket path is possible as well:
312272
```swift
313-
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
314-
defer {
315-
// Shutdown is guaranteed to work if it's done precisely once (which is the case here).
316-
try! httpClient.syncShutdown()
317-
}
318-
319-
httpClient.execute(
273+
HTTPClient.shared.execute(
320274
.POST,
321275
secureSocketPath: "/tmp/myServer.socket",
322276
urlPath: "/path/to/resource",

0 commit comments

Comments
 (0)