Skip to content

Commit f0a70cb

Browse files
fabianfettdnadoba
authored andcommitted
Work on async/await continues
1 parent dbba980 commit f0a70cb

File tree

5 files changed

+172
-22
lines changed

5 files changed

+172
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2021 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+
import struct Foundation.URL
16+
import NIOHTTP1
17+
18+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
19+
extension AsyncRequest {
20+
21+
struct ValidationResult {
22+
let requestFramingMetadata: RequestFramingMetadata
23+
let poolKey: ConnectionPool.Key
24+
let head: HTTPRequestHead
25+
}
26+
27+
func validate() throws -> ValidationResult {
28+
29+
guard let url = URL(string: self.url) else {
30+
throw HTTPClientError.invalidURL
31+
}
32+
33+
guard let urlScheme = url.scheme?.lowercased() else {
34+
throw HTTPClientError.emptyScheme
35+
}
36+
37+
let kind = try HTTPClient.Request.Kind(forScheme: urlScheme)
38+
let useTLS: Bool = urlScheme == "https" || urlScheme == "https+unix"
39+
40+
let poolKey = try ConnectionPool.Key(
41+
scheme: .init(string: urlScheme),
42+
host: kind.hostFromURL(url),
43+
port: url.port ?? (useTLS ? 443 : 80),
44+
unixPath: kind.socketPathFromURL(url),
45+
tlsConfiguration: nil
46+
)
47+
48+
try self.headers.validateFieldNames()
49+
50+
var head = HTTPRequestHead(version: .http1_1, method: self.method, uri: url.uri, headers: self.headers)
51+
52+
// if no host header was set, let's pick
53+
if !head.headers.contains(name: "host") {
54+
guard let urlHost = url.host else {
55+
throw HTTPClientError.emptyHost
56+
}
57+
head.headers.add(name: "host", value: urlHost)
58+
}
59+
60+
let encodings = head.headers[canonicalForm: "Transfer-Encoding"].map { $0.lowercased() }
61+
if encodings.contains("identity") {
62+
throw HTTPClientError.identityCodingIncorrectlyPresent
63+
}
64+
65+
head.headers.remove(name: "Transfer-Encoding")
66+
67+
guard let body = self.body else {
68+
head.headers.remove(name: "Content-Length")
69+
// if we don't have a body we might not need to send the Content-Length field
70+
// https://tools.ietf.org/html/rfc7230#section-3.3.2
71+
switch method {
72+
case .GET, .HEAD, .DELETE, .CONNECT, .TRACE:
73+
// A user agent SHOULD NOT send a Content-Length header field when the request
74+
// message does not contain a payload body and the method semantics do not
75+
// anticipate such a body.
76+
return ValidationResult(
77+
requestFramingMetadata: .init(connectionClose: !head.isKeepAlive, body: .none),
78+
poolKey: poolKey,
79+
head: head
80+
)
81+
default:
82+
// A user agent SHOULD send a Content-Length in a request message when
83+
// no Transfer-Encoding is sent and the request method defines a meaning
84+
// for an enclosed payload body.
85+
head.headers.add(name: "Content-Length", value: "0")
86+
return ValidationResult(
87+
requestFramingMetadata: .init(connectionClose: !head.isKeepAlive, body: .none),
88+
poolKey: poolKey,
89+
head: head
90+
)
91+
}
92+
}
93+
94+
if case .TRACE = method {
95+
// A client MUST NOT send a message body in a TRACE request.
96+
// https://tools.ietf.org/html/rfc7230#section-4.3.8
97+
throw HTTPClientError.traceRequestWithBody
98+
}
99+
100+
guard (encodings.lazy.filter { $0 == "chunked" }.count <= 1) else {
101+
throw HTTPClientError.chunkedSpecifiedMultipleTimes
102+
}
103+
104+
if encodings.isEmpty {
105+
switch self.body {
106+
case .some(.byteBuffer(let byteBuffer)):
107+
head.headers.add(name: "content-length", value: "\(byteBuffer.readableBytes)")
108+
case .some(bytes(let sequence)):
109+
// if we have a content length header, we assume this was set correctly
110+
if head.headers.contains(name: "content-length") {
111+
112+
} else {
113+
head.headers.add(name: "transfer-encoding", value: "chunked")
114+
}
115+
116+
}
117+
118+
119+
}
120+
}
121+
}

Sources/AsyncHTTPClient/AsyncAwait/AsyncRequest.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ extension HTTPClient {
183183
func execute(_ request: AsyncRequest, deadline: NIODeadline, logger: Logger) async throws -> AsyncResponse {
184184
let bag = AsyncRequestBag(
185185
request: request,
186+
requestOptions: .init(idleReadTimeout: nil, ignoreUncleanSSLShutdown: false),
186187
logger: logger,
187188
connectionDeadline: .now() + .seconds(10),
188-
eventLoopPreference: .indifferent
189+
preferredEventLoop: self.eventLoopGroup.next()
189190
)
190191

191192
return try await withTaskCancellationHandler {
@@ -195,7 +196,7 @@ extension HTTPClient {
195196
async let result = bag.result()
196197

197198
// second throw it onto the connection pool for execution
198-
// self.pool.execute(bag)
199+
self.poolManager.executeRequest(bag)
199200

200201
// third await result
201202
return try await result

Sources/AsyncHTTPClient/AsyncAwait/AsyncRequestBag.swift

+16-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import Logging
1616
import NIO
17+
import NIOSSL
1718
import NIOHTTP1
1819

1920
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
@@ -24,24 +25,26 @@ actor AsyncRequestBag {
2425
let logger: Logger
2526

2627
let requestHead: HTTPRequestHead
28+
let requestOptions: RequestOptions
2729
let requestFramingMetadata: RequestFramingMetadata
2830

29-
let idleReadTimeout: TimeAmount?
3031
let connectionDeadline: NIODeadline
31-
let eventLoopPreference: HTTPClient.EventLoopPreference
32+
let preferredEventLoop: EventLoop
33+
let poolKey: ConnectionPool.Key
3234

3335
private var state: StateMachine = .init()
3436
private var isCancelled = false
3537

3638
init(request: AsyncRequest,
39+
requestOptions: RequestOptions,
3740
logger: Logger,
3841
connectionDeadline: NIODeadline,
39-
eventLoopPreference: HTTPClient.EventLoopPreference) {
42+
preferredEventLoop: EventLoop) {
4043
self.request = request
44+
self.requestOptions = requestOptions
4145
self.logger = logger
42-
self.idleReadTimeout = nil
4346
self.connectionDeadline = connectionDeadline
44-
self.eventLoopPreference = eventLoopPreference
47+
self.preferredEventLoop = preferredEventLoop
4548

4649
self.requestHead = HTTPRequestHead(
4750
version: .http1_1,
@@ -248,6 +251,14 @@ actor AsyncRequestBag {
248251

249252
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
250253
extension AsyncRequestBag: HTTPSchedulableRequest {
254+
nonisolated var tlsConfiguration: TLSConfiguration? {
255+
return nil
256+
}
257+
258+
nonisolated var requiredEventLoop: EventLoop? {
259+
return nil
260+
}
261+
251262
nonisolated func requestWasQueued(_ scheduler: HTTPRequestScheduler) {
252263
Task.detached {
253264
await self.requestWasQueued0(scheduler)

Sources/AsyncHTTPClient/ConnectionPool.swift

+32-14
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,28 @@ enum ConnectionPool {
2020
/// connection providers associated to a certain request in constant time.
2121
struct Key: Hashable, CustomStringConvertible {
2222
init(_ request: HTTPClient.Request) {
23-
switch request.scheme {
24-
case "http":
25-
self.scheme = .http
26-
case "https":
27-
self.scheme = .https
28-
case "unix":
29-
self.scheme = .unix
30-
case "http+unix":
31-
self.scheme = .http_unix
32-
case "https+unix":
33-
self.scheme = .https_unix
34-
default:
35-
fatalError("HTTPClient.Request scheme should already be a valid one")
36-
}
23+
self.scheme = Scheme(string: request.scheme)
3724
self.port = request.port
3825
self.host = request.host
3926
self.unixPath = request.socketPath
4027
if let tls = request.tlsConfiguration {
4128
self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls)
4229
}
4330
}
31+
32+
init(
33+
scheme: Scheme,
34+
host: String,
35+
port: Int,
36+
unixPath: String,
37+
tlsConfiguration: BestEffortHashableTLSConfiguration?
38+
) {
39+
self.scheme = scheme
40+
self.host = host
41+
self.port = port
42+
self.unixPath = unixPath
43+
self.tlsConfiguration = tlsConfiguration
44+
}
4445

4546
var scheme: Scheme
4647
var host: String
@@ -54,6 +55,23 @@ enum ConnectionPool {
5455
case unix
5556
case http_unix
5657
case https_unix
58+
59+
init(string: String) {
60+
switch string {
61+
case "http":
62+
self = .http
63+
case "https":
64+
self = .https
65+
case "unix":
66+
self = .unix
67+
case "http+unix":
68+
self = .http_unix
69+
case "https+unix":
70+
self = .https_unix
71+
default:
72+
fatalError("HTTPClient.Request scheme should already be a valid one")
73+
}
74+
}
5775

5876
var requiresTLS: Bool {
5977
switch self {

Tests/AsyncHTTPClientTests/AsyncRequestTests.swift

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import _NIOConcurrency
1615
@testable import AsyncHTTPClient
1716
import Logging
1817
import NIO

0 commit comments

Comments
 (0)