Skip to content

Commit e6434b3

Browse files
authored
feat: async http client (#22)
* Try async * Async http client * mark sendable * Shutdown client * Use sync shutdown * Refactor body * Fix text body * Fix concurrency issues * Update readme with isolated
1 parent 2f655ca commit e6434b3

12 files changed

Lines changed: 124 additions & 67 deletions

File tree

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ let package = Package(
1616
.package(url: "https://github.com/apple/swift-crypto", from: "3.0.0"),
1717
.package(
1818
url: "https://github.com/swift-server/swift-aws-lambda-runtime", from: "1.0.0-alpha.2"),
19+
.package(url: "https://github.com/swift-server/async-http-client", from: "1.20.1"),
1920
.package(url: "https://github.com/vapor/vapor", from: "4.0.0"),
2021
],
2122
targets: [
2223
.target(
2324
name: "Vercel",
2425
dependencies: [
2526
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
27+
.product(name: "AsyncHTTPClient", package: "async-http-client"),
2628
.product(name: "Crypto", package: "swift-crypto"),
2729
],
2830
swiftSettings: [

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import Vercel
4646
@main
4747
struct App: ExpressHandler {
4848

49-
static func configure(router: Router) async throws {
49+
static func configure(router: isolated Router) async throws {
5050
router.get("/") { req, res in
5151
res.status(.ok).send("Hello, Swift")
5252
}

Sources/Vercel/Environment.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ public struct VercelEnvironment: Sendable {
2828

2929
extension VercelEnvironment {
3030

31-
public static var edgeConfig = Self["EDGE_CONFIG"]!
31+
public static let edgeConfig = Self["EDGE_CONFIG"]!
3232

33-
public static var vercelEnvironment = Self["VERCEL_ENV", default: "dev"]
33+
public static let vercelEnvironment = Self["VERCEL_ENV", default: "dev"]
3434

35-
public static var vercelHostname = Self["VERCEL_URL", default: "localhost"]
35+
public static let vercelHostname = Self["VERCEL_URL", default: "localhost"]
3636

37-
public static var vercelRegion = Self["VERCEL_REGION", default: "dev1"]
37+
public static let vercelRegion = Self["VERCEL_REGION", default: "dev1"]
3838
}

Sources/Vercel/Fetch/Fetch.swift

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
//
77

88
import Foundation
9-
#if canImport(FoundationNetworking)
10-
import FoundationNetworking
11-
#endif
9+
import AsyncHTTPClient
1210

1311
public enum FetchError: Error, Sendable {
1412
case invalidResponse
1513
case invalidURL
1614
case timeout
15+
case invalidLambdaContext
1716
}
1817

1918
public func fetch(_ request: FetchRequest) async throws -> FetchResponse {
@@ -42,56 +41,44 @@ public func fetch(_ request: FetchRequest) async throws -> FetchResponse {
4241
}
4342

4443
// Set request resources
45-
var httpRequest = URLRequest(url: url)
44+
var httpRequest = HTTPClientRequest(url: url.absoluteString)
4645

4746
// Set request method
48-
httpRequest.httpMethod = request.method.rawValue
49-
50-
// Set the timeout interval
51-
if let timeoutInterval = request.timeoutInterval {
52-
httpRequest.timeoutInterval = timeoutInterval
53-
}
47+
httpRequest.method = .init(rawValue: request.method.rawValue)
5448

5549
// Set default content type based on body
5650
if let contentType = request.body?.defaultContentType {
5751
let name = HTTPHeaderKey.contentType.rawValue
58-
httpRequest.setValue(request.headers[name] ?? contentType, forHTTPHeaderField: name)
52+
httpRequest.headers.add(name: name, value: request.headers[name] ?? contentType)
5953
}
6054

6155
// Set headers
6256
for (key, value) in request.headers {
63-
httpRequest.setValue(value, forHTTPHeaderField: key)
57+
httpRequest.headers.add(name: key, value: value)
6458
}
6559

6660
// Write bytes to body
6761
switch request.body {
6862
case .bytes(let bytes):
69-
httpRequest.httpBody = Data(bytes)
63+
httpRequest.body = .bytes(bytes)
7064
case .data(let data):
71-
httpRequest.httpBody = data
65+
httpRequest.body = .bytes(data)
7266
case .text(let text):
73-
httpRequest.httpBody = Data(text.utf8)
67+
httpRequest.body = .bytes(text.utf8, length: .known(text.utf8.count))
7468
case .json(let json):
75-
httpRequest.httpBody = json
69+
httpRequest.body = .bytes(json)
7670
case .none:
7771
break
7872
}
7973

80-
let (data, response): (Data, HTTPURLResponse) = try await withCheckedThrowingContinuation { continuation in
81-
let task = URLSession.shared.dataTask(with: httpRequest) { data, response, error in
82-
if let data, let response = response as? HTTPURLResponse {
83-
continuation.resume(returning: (data, response))
84-
} else {
85-
continuation.resume(throwing: error ?? FetchError.invalidResponse)
86-
}
87-
}
88-
task.resume()
89-
}
74+
let httpClient = request.httpClient ?? HTTPClient.vercelClient
75+
76+
let response = try await httpClient.execute(httpRequest, timeout: request.timeout ?? .seconds(60))
9077

9178
return FetchResponse(
92-
body: data,
93-
headers: response.allHeaderFields as! [String: String],
94-
status: response.statusCode,
79+
body: response.body,
80+
headers: response.headers.reduce(into: [:]) { $0[$1.name] = $1.value },
81+
status: .init(response.status.code),
9582
url: url
9683
)
9784
}
@@ -108,3 +95,39 @@ public func fetch(_ urlPath: String, _ options: FetchRequest.Options = .options(
10895
let request = FetchRequest(url, options)
10996
return try await fetch(request)
11097
}
98+
99+
extension HTTPClient {
100+
101+
fileprivate static let vercelClient = HTTPClient(
102+
eventLoopGroup: HTTPClient.defaultEventLoopGroup,
103+
configuration: .vercelConfiguration
104+
)
105+
}
106+
107+
extension HTTPClient.Configuration {
108+
/// The ``HTTPClient/Configuration`` for ``HTTPClient/shared`` which tries to mimic the platform's default or prevalent browser as closely as possible.
109+
///
110+
/// Don't rely on specific values of this configuration as they're subject to change. You can rely on them being somewhat sensible though.
111+
///
112+
/// - note: At present, this configuration is nowhere close to a real browser configuration but in case of disagreements we will choose values that match
113+
/// the default browser as closely as possible.
114+
///
115+
/// Platform's default/prevalent browsers that we're trying to match (these might change over time):
116+
/// - macOS: Safari
117+
/// - iOS: Safari
118+
/// - Android: Google Chrome
119+
/// - Linux (non-Android): Google Chrome
120+
fileprivate static var vercelConfiguration: HTTPClient.Configuration {
121+
// To start with, let's go with these values. Obtained from Firefox's config.
122+
return HTTPClient.Configuration(
123+
certificateVerification: .fullVerification,
124+
redirectConfiguration: .follow(max: 20, allowCycles: false),
125+
timeout: Timeout(connect: .seconds(90), read: .seconds(90)),
126+
connectionPool: .seconds(600),
127+
proxy: nil,
128+
ignoreUncleanSSLShutdown: false,
129+
decompression: .enabled(limit: .ratio(10)),
130+
backgroundActivityLogger: nil
131+
)
132+
}
133+
}

Sources/Vercel/Fetch/FetchRequest.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
// Created by Andrew Barba on 1/22/23.
66
//
77

8+
import AsyncHTTPClient
9+
import NIOCore
10+
811
public struct FetchRequest: Sendable {
912

1013
public var url: URL
@@ -17,15 +20,18 @@ public struct FetchRequest: Sendable {
1720

1821
public var body: Body?
1922

20-
public var timeoutInterval: TimeInterval? = nil
23+
public var timeout: TimeAmount? = nil
24+
25+
public var httpClient: HTTPClient? = nil
2126

2227
public init(_ url: URL, _ options: Options = .options()) {
2328
self.url = url
2429
self.method = options.method
2530
self.headers = options.headers
2631
self.searchParams = options.searchParams
2732
self.body = options.body
28-
self.timeoutInterval = options.timeoutInterval
33+
self.timeout = options.timeout
34+
self.httpClient = options.httpClient
2935
}
3036
}
3137

@@ -41,21 +47,25 @@ extension FetchRequest {
4147

4248
public var searchParams: [String: String] = [:]
4349

44-
public var timeoutInterval: TimeInterval? = nil
50+
public var timeout: TimeAmount? = nil
51+
52+
public var httpClient: HTTPClient? = nil
4553

4654
public static func options(
4755
method: HTTPMethod = .GET,
4856
body: Body? = nil,
4957
headers: [String: String] = [:],
5058
searchParams: [String: String] = [:],
51-
timeoutInterval: TimeInterval? = nil
59+
timeout: TimeAmount? = nil,
60+
httpClient: HTTPClient? = nil
5261
) -> Options {
5362
return Options(
5463
method: method,
5564
body: body,
5665
headers: headers,
5766
searchParams: searchParams,
58-
timeoutInterval: timeoutInterval
67+
timeout: timeout,
68+
httpClient: httpClient
5969
)
6070
}
6171
}

Sources/Vercel/Fetch/FetchResponse.swift

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
// Created by Andrew Barba on 1/22/23.
66
//
77

8+
import AsyncHTTPClient
9+
import NIOCore
10+
import NIOFoundationCompat
11+
812
public struct FetchResponse: Sendable {
913

10-
public let body: Data
14+
public let body: HTTPClientResponse.Body
1115

1216
public let headers: [String: String]
1317

@@ -26,27 +30,32 @@ extension FetchResponse {
2630
extension FetchResponse {
2731

2832
public func decode<T>(decoder: JSONDecoder = .init()) async throws -> T where T: Decodable & Sendable {
29-
return try decoder.decode(T.self, from: body)
33+
let bytes = try await self.bytes()
34+
return try decoder.decode(T.self, from: bytes)
3035
}
3136

3237
public func decode<T>(_ type: T.Type, decoder: JSONDecoder = .init()) async throws -> T where T: Decodable & Sendable {
33-
return try decoder.decode(type, from: body)
38+
let bytes = try await self.bytes()
39+
return try decoder.decode(type, from: bytes)
3440
}
3541

3642
public func json() async throws -> Any {
37-
return try JSONSerialization.jsonObject(with: body)
43+
let bytes = try await self.bytes()
44+
return try JSONSerialization.jsonObject(with: bytes)
3845
}
3946

4047
public func jsonObject() async throws -> [String: Any] {
41-
return try JSONSerialization.jsonObject(with: body) as! [String: Any]
48+
let bytes = try await self.bytes()
49+
return try JSONSerialization.jsonObject(with: bytes) as! [String: Any]
4250
}
4351

4452
public func jsonArray() async throws -> [Any] {
45-
return try JSONSerialization.jsonObject(with: body) as! [Any]
53+
let bytes = try await self.bytes()
54+
return try JSONSerialization.jsonObject(with: bytes) as! [Any]
4655
}
4756

4857
public func formValues() async throws -> [String: String] {
49-
let query = String(data: body, encoding: .utf8)!
58+
let query = try await self.text()
5059
let components = URLComponents(string: "?\(query)")
5160
let queryItems = components?.queryItems ?? []
5261
return queryItems.reduce(into: [:]) { values, item in
@@ -55,14 +64,16 @@ extension FetchResponse {
5564
}
5665

5766
public func text() async throws -> String {
58-
return String(data: body, encoding: .utf8)!
67+
var bytes = try await self.bytes()
68+
return bytes.readString(length: bytes.readableBytes) ?? ""
5969
}
6070

6171
public func data() async throws -> Data {
62-
return body
72+
var bytes = try await self.bytes()
73+
return bytes.readData(length: bytes.readableBytes) ?? .init()
6374
}
6475

65-
public func bytes() async throws -> [UInt8] {
66-
return Array(body)
76+
public func bytes(upTo maxBytes: Int = .max) async throws -> ByteBuffer {
77+
return try await body.collect(upTo: maxBytes)
6778
}
6879
}

Sources/Vercel/Handlers/ExpressHandler.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import AWSLambdaRuntime
1010
public protocol ExpressHandler: RequestHandler {
1111

1212
static var basePath: String { get }
13-
14-
static func configure(router: Router) async throws
13+
14+
static func configure(router: isolated Router) async throws
1515
}
1616

1717
extension ExpressHandler {
@@ -26,18 +26,24 @@ extension ExpressHandler {
2626
// Configure router in user code
2727
try await configure(router: router)
2828
// Cache the app instance
29-
Shared.router = router
29+
await Shared.default.setRouter(router)
3030
}
3131

3232
public func onRequest(_ req: Request) async throws -> Response {
33-
guard let router = Shared.router else {
33+
guard let router = await Shared.default.router else {
3434
return .status(.serviceUnavailable).send("Express router not configured")
3535
}
3636
return try await router.run(req)
3737
}
3838
}
3939

40-
fileprivate struct Shared {
40+
fileprivate actor Shared {
41+
42+
static let `default` = Shared()
4143

42-
static var router: Router?
44+
var router: Router?
45+
46+
func setRouter(_ router: Router) {
47+
self.router = router
48+
}
4349
}

Sources/Vercel/Handlers/RequestHandler.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import AWSLambdaRuntime
99
import NIOCore
1010

11-
public protocol RequestHandler: EventLoopLambdaHandler where Event == InvokeEvent, Output == Response {
11+
public protocol RequestHandler: Sendable & EventLoopLambdaHandler where Event == InvokeEvent, Output == Response {
1212

1313
func onRequest(_ req: Request) async throws -> Response
1414

@@ -24,7 +24,9 @@ extension RequestHandler {
2424
let data = Data(event.body.utf8)
2525
let payload = try JSONDecoder().decode(InvokeEvent.Payload.self, from: data)
2626
let req = Request(payload, in: context)
27-
return try await onRequest(req)
27+
return try await Request.$current.withValue(req) {
28+
return try await onRequest(req)
29+
}
2830
}
2931
}
3032

Sources/Vercel/JWT/JWT.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public struct JWT: Sendable {
5454
}
5555

5656
public init(
57-
claims: [String: Any],
57+
claims: [String: Sendable],
5858
secret: String,
5959
algorithm: Algorithm = .hs256,
6060
issuedAt: Date = .init(),
@@ -63,12 +63,12 @@ public struct JWT: Sendable {
6363
subject: String? = nil,
6464
identifier: String? = nil
6565
) throws {
66-
let header: [String: Any] = [
66+
let header: [String: Sendable] = [
6767
"alg": algorithm.rawValue,
6868
"typ": "JWT"
6969
]
7070

71-
var properties: [String: Any] = [
71+
var properties: [String: Sendable] = [
7272
"iat": floor(issuedAt.timeIntervalSince1970)
7373
]
7474

@@ -191,9 +191,9 @@ extension JWT {
191191
}
192192
}
193193

194-
private func decodeJWTPart(_ value: String) throws -> [String: Any] {
194+
private func decodeJWTPart(_ value: String) throws -> [String: Sendable] {
195195
let bodyData = try base64UrlDecode(value)
196-
guard let json = try JSONSerialization.jsonObject(with: bodyData, options: []) as? [String: Any] else {
196+
guard let json = try JSONSerialization.jsonObject(with: bodyData, options: []) as? [String: Sendable] else {
197197
throw JWTError.invalidJSON
198198
}
199199
return json

0 commit comments

Comments
 (0)