Skip to content

Commit 7ec050b

Browse files
committed
Add async/await proposal
1 parent da5da25 commit 7ec050b

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed

docs/async-await.md

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Proposal: Async-await support
2+
3+
## Introduction
4+
5+
With the introduction of [async/await][SE-0296] in Swift 5.5, it is now possible to write asynchronous code without the need for callbacks.
6+
7+
Language support for [`AsyncSequence`][SE-0298] also allows for writing functions that return values over time.
8+
9+
We would like to explore how we could offer APIs that make use of these new language features to allow users to run HTTPRequest using these new idioms.
10+
11+
This proposal describes what these APIs could look like and explores some of the potential usability concerns.
12+
13+
## Proposed API additions
14+
15+
### New `AsyncRequest` type
16+
17+
The proposed new `AsyncRequest` shall be a simple swift structure.
18+
19+
```swift
20+
struct AsyncRequest {
21+
/// The requests url.
22+
var url: String
23+
24+
/// The request's HTTPMethod
25+
var method: HTTPMethod
26+
27+
/// The request's headers
28+
var headers: HTTPHeaders
29+
30+
/// The request's body
31+
var body: Body?
32+
33+
init(url: String) {
34+
self.url = url
35+
self.method = .GET
36+
self.headers = .init()
37+
self.body = .none
38+
}
39+
}
40+
```
41+
42+
A notable change from the current [`HTTPRequest`][HTTPRequest] is that the url is not of type `URL`. This makes the creation of a request non throwing. Existing issues regarding current API:
43+
44+
- [HTTPClient.Request.url is a let constant][issue-395]
45+
- [refactor to make request non-throwing](https://github.com/swift-server/async-http-client/pull/56)
46+
- [improve request validation](https://github.com/swift-server/async-http-client/pull/67)
47+
48+
The url validation will become part of the normal request validation that occurs when the request is scheduled on the `HTTPClient`. If the user supplies a request with an invalid url, the http client, will reject the request.
49+
50+
In normal try/catch flows this should not change the control flow:
51+
52+
```swift
53+
do {
54+
var request = AsyncRequest(url: "invalidurl")
55+
try await httpClient.execute(request, deadline: .now() + .seconds(3))
56+
} catch {
57+
print(error)
58+
}
59+
```
60+
61+
If the library code throws from the AsyncRequest creation or the request invocation the user will, in normal use cases, handle the error in the same catch block.
62+
63+
#### Body streaming
64+
65+
The new AsyncRequest has a new body type, that is wrapper around an internal enum. This allows us to evolve this type for use-cases that we are not aware of today.
66+
67+
```swift
68+
public struct Body {
69+
static func bytes<S: Sequence>(_ sequence: S) -> Body where S.Element == UInt8
70+
71+
static func stream<S: AsyncSequence>(_ sequence: S) -> Body where S.Element == ByteBuffer
72+
73+
static func stream<S: AsyncSequence>(_ sequence: S) -> Body where S.Element == UInt8
74+
}
75+
```
76+
77+
The main difference to today's `Request.Body` type is the lack of a `StreamWriter` for streaming scenarios. The existing StreamWriter offered the user an API to write into (thus the user was in control of when writing happened). The new `AsyncRequest.Body` uses `AsyncSequence`s to stream requests. By iterating over the provided AsyncSequence, the HTTPClient is in control when writes happen, and can ask for more data efficiently.
78+
79+
Using the `AsyncSequence` from the Swift standard library as our upload stream mechanism dramatically reduces the learning curve for new users.
80+
81+
### New `AsyncResponse` type
82+
83+
The `AsyncResponse` looks more similar to the existing `Response` type. The biggest difference is again the `body` property, which is now an `AsyncSequence` of `ByteBuffer`s instead of a single optional `ByteBuffer?`. This will make every response on AsyncHTTPClient streaming by default.
84+
85+
```swift
86+
public struct AsyncResponse {
87+
/// the used http version
88+
public var version: HTTPVersion
89+
/// the http response status
90+
public var status: HTTPResponseStatus
91+
/// the response headers
92+
public var headers: HTTPHeaders
93+
/// the response payload as an AsyncSequence
94+
public var body: Body
95+
}
96+
97+
extension AsyncResponse {
98+
public struct Body: AsyncSequence {
99+
public typealias Element = ByteBuffer
100+
public typealias AsyncIterator = Iterator
101+
102+
public struct Iterator: AsyncIteratorProtocol {
103+
public typealias Element = ByteBuffer
104+
105+
public func next() async throws -> ByteBuffer?
106+
}
107+
108+
public func makeAsyncIterator() -> Iterator
109+
}
110+
}
111+
```
112+
113+
At a later point we could add trailers to the AsyncResponse as effectful properties.
114+
115+
```swift
116+
public var trailers: HTTPHeaders { async throws }
117+
```
118+
119+
However we will need to make sure that the user has consumed the body stream completely before, calling the trailers, because otherwise we might run into a situation from which we can not progress forward:
120+
121+
```swift
122+
do {
123+
var request = AsyncRequest(url: "https://swift.org/")
124+
let response = try await httpClient.execute(request, deadline: .now() + .seconds(3))
125+
126+
var trailers = try await response.trailers // can not move forward since body must be consumed before.
127+
} catch {
128+
print(error)
129+
}
130+
131+
```
132+
133+
### New invocation
134+
135+
The new way to invoke a request shall look like this:
136+
137+
```swift
138+
extension HTTPClient {
139+
func execute(_ request: AsyncRequest, deadline: NIODeadline) async throws -> AsyncResponse
140+
}
141+
```
142+
143+
- **Why do we have a deadline in the function signature?**
144+
Task deadlines are not part of the Swift 5.5 release. However we think that they are an important tool to not overload the http client accidentally. For this reason we will not default them.
145+
- **What happened to the Logger?** We will use Task locals to propagate the logger metadata. @slashmo and @ktoso are currently working on this.
146+
- **How does cancellation work?** Cancellation works by cancelling the surrounding task:
147+
148+
```swift
149+
let task = Task {
150+
let response = try await httpClient.execute(request, deadline: .distantFuture)
151+
}
152+
153+
Task.sleep(500 * 1000 * 1000) // wait half a second
154+
task.cancel() // cancel the task after half a second
155+
```
156+
157+
- **What happens with all the other configuration options?** Currently users can configure a TLSConfiguration on a request. This API doesn't expose this option. We hope to create a three layer model in the future. For this reason, we currently don't want to add per request configuration on the request invocation. More info can be found in the issue: [RFC: design suggestion: Make this a "3-tier library"][issue-392]
158+
159+
160+
[SE-0296]: https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
161+
[SE-0298]: https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md
162+
[SE-0310]: https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md
163+
[SE-0314]: https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md
164+
165+
[issue-392]: https://github.com/swift-server/async-http-client/issues/392
166+
[issue-395]: https://github.com/swift-server/async-http-client/issues/395
167+
168+
[HTTPRequest]: https://github.com/swift-server/async-http-client/blob/main/Sources/AsyncHTTPClient/HTTPHandler.swift#L96-L318

0 commit comments

Comments
 (0)