Skip to content

Commit 134aebf

Browse files
authored
Merge branch 'main' into debug-initializer
2 parents 77ffedf + 01908f4 commit 134aebf

22 files changed

+803
-91
lines changed

.github/workflows/main.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@ jobs:
1414
linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
1515
linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
1616
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
17-
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
17+
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
1818
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
19+
20+
static-sdk:
21+
name: Static SDK
22+
# Workaround https://github.com/nektos/act/issues/1875
23+
uses: apple/swift-nio/.github/workflows/static_sdk.yml@main

.github/workflows/pull_request.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ jobs:
1717
linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
1818
linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
1919
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
20-
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
20+
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
2121
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
2222

2323
cxx-interop:
2424
name: Cxx interop
2525
uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
26+
27+
static-sdk:
28+
name: Static SDK
29+
# Workaround https://github.com/nektos/act/issues/1875
30+
uses: apple/swift-nio/.github/workflows/static_sdk.yml@main

Package.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,16 @@ let package = Package(
8686

8787
// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
8888
for target in package.targets {
89-
if target.type != .plugin {
89+
switch target.type {
90+
case .regular, .test, .executable:
9091
var settings = target.swiftSettings ?? []
9192
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
9293
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
9394
target.swiftSettings = settings
95+
case .macro, .plugin, .system, .binary:
96+
() // not applicable
97+
@unknown default:
98+
() // we don't know what to do here, do nothing
9499
}
95100
}
96101
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //

Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift

+31-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ extension HTTPClient {
2626
/// - request: HTTP request to execute.
2727
/// - deadline: Point in time by which the request must complete.
2828
/// - logger: The logger to use for this request.
29+
///
30+
/// - warning: This method may violates Structured Concurrency because it returns a `HTTPClientResponse` that needs to be
31+
/// streamed by the user. This means the request, the connection and other resources are still alive when the request returns.
32+
///
2933
/// - Returns: The response to the request. Note that the `body` of the response may not yet have been fully received.
3034
public func execute(
3135
_ request: HTTPClientRequest,
@@ -51,6 +55,10 @@ extension HTTPClient {
5155
/// - request: HTTP request to execute.
5256
/// - timeout: time the the request has to complete.
5357
/// - logger: The logger to use for this request.
58+
///
59+
/// - warning: This method may violates Structured Concurrency because it returns a `HTTPClientResponse` that needs to be
60+
/// streamed by the user. This means the request, the connection and other resources are still alive when the request returns.
61+
///
5462
/// - Returns: The response to the request. Note that the `body` of the response may not yet have been fully received.
5563
public func execute(
5664
_ request: HTTPClientRequest,
@@ -67,6 +75,8 @@ extension HTTPClient {
6775

6876
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
6977
extension HTTPClient {
78+
/// - warning: This method may violates Structured Concurrency because it returns a `HTTPClientResponse` that needs to be
79+
/// streamed by the user. This means the request, the connection and other resources are still alive when the request returns.
7080
private func executeAndFollowRedirectsIfNeeded(
7181
_ request: HTTPClientRequest,
7282
deadline: NIODeadline,
@@ -75,11 +85,29 @@ extension HTTPClient {
7585
) async throws -> HTTPClientResponse {
7686
var currentRequest = request
7787
var currentRedirectState = redirectState
88+
var history: [HTTPClientRequestResponse] = []
7889

7990
// this loop is there to follow potential redirects
8091
while true {
8192
let preparedRequest = try HTTPClientRequest.Prepared(currentRequest, dnsOverride: configuration.dnsOverride)
82-
let response = try await self.executeCancellable(preparedRequest, deadline: deadline, logger: logger)
93+
let response = try await {
94+
var response = try await self.executeCancellable(preparedRequest, deadline: deadline, logger: logger)
95+
96+
history.append(
97+
.init(
98+
request: currentRequest,
99+
responseHead: .init(
100+
version: response.version,
101+
status: response.status,
102+
headers: response.headers
103+
)
104+
)
105+
)
106+
107+
response.history = history
108+
109+
return response
110+
}()
83111

84112
guard var redirectState = currentRedirectState else {
85113
// a `nil` redirectState means we should not follow redirects
@@ -116,6 +144,8 @@ extension HTTPClient {
116144
}
117145
}
118146

147+
/// - warning: This method may violates Structured Concurrency because it returns a `HTTPClientResponse` that needs to be
148+
/// streamed by the user. This means the request, the connection and other resources are still alive when the request returns.
119149
private func executeCancellable(
120150
_ request: HTTPClientRequest.Prepared,
121151
deadline: NIODeadline,

Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift

+44-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import NIOCore
1616
import NIOHTTP1
1717

18+
import struct Foundation.URL
19+
1820
/// A representation of an HTTP response for the Swift Concurrency HTTPClient API.
1921
///
2022
/// This object is similar to ``HTTPClient/Response``, but used for the Swift Concurrency API.
@@ -32,6 +34,18 @@ public struct HTTPClientResponse: Sendable {
3234
/// The body of this HTTP response.
3335
public var body: Body
3436

37+
/// The history of all requests and responses in redirect order.
38+
public var history: [HTTPClientRequestResponse]
39+
40+
/// The target URL (after redirects) of the response.
41+
public var url: URL? {
42+
guard let lastRequestURL = self.history.last?.request.url else {
43+
return nil
44+
}
45+
46+
return URL(string: lastRequestURL)
47+
}
48+
3549
@inlinable public init(
3650
version: HTTPVersion = .http1_1,
3751
status: HTTPResponseStatus = .ok,
@@ -42,14 +56,30 @@ public struct HTTPClientResponse: Sendable {
4256
self.status = status
4357
self.headers = headers
4458
self.body = body
59+
self.history = []
60+
}
61+
62+
@inlinable public init(
63+
version: HTTPVersion = .http1_1,
64+
status: HTTPResponseStatus = .ok,
65+
headers: HTTPHeaders = [:],
66+
body: Body = Body(),
67+
history: [HTTPClientRequestResponse] = []
68+
) {
69+
self.version = version
70+
self.status = status
71+
self.headers = headers
72+
self.body = body
73+
self.history = history
4574
}
4675

4776
init(
4877
requestMethod: HTTPMethod,
4978
version: HTTPVersion,
5079
status: HTTPResponseStatus,
5180
headers: HTTPHeaders,
52-
body: TransactionBody
81+
body: TransactionBody,
82+
history: [HTTPClientRequestResponse]
5383
) {
5484
self.init(
5585
version: version,
@@ -64,11 +94,23 @@ public struct HTTPClientResponse: Sendable {
6494
status: status
6595
)
6696
)
67-
)
97+
),
98+
history: history
6899
)
69100
}
70101
}
71102

103+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
104+
public struct HTTPClientRequestResponse: Sendable {
105+
public var request: HTTPClientRequest
106+
public var responseHead: HTTPResponseHead
107+
108+
public init(request: HTTPClientRequest, responseHead: HTTPResponseHead) {
109+
self.request = request
110+
self.responseHead = responseHead
111+
}
112+
}
113+
72114
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
73115
extension HTTPClientResponse {
74116
/// A representation of the response body for an HTTP response.

Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ extension Transaction: HTTPExecutableRequest {
242242
version: head.version,
243243
status: head.status,
244244
headers: head.headers,
245-
body: body
245+
body: body,
246+
history: []
246247
)
247248
continuation.resume(returning: response)
248249
}

Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ClientChannelHandler.swift

+2
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
314314
let oldRequest = self.request!
315315
self.request = nil
316316
self.runTimeoutAction(.clearIdleReadTimeoutTimer, context: context)
317+
self.runTimeoutAction(.clearIdleWriteTimeoutTimer, context: context)
317318

318319
switch finalAction {
319320
case .close:
@@ -353,6 +354,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
353354
let oldRequest = self.request!
354355
self.request = nil
355356
self.runTimeoutAction(.clearIdleReadTimeoutTimer, context: context)
357+
self.runTimeoutAction(.clearIdleWriteTimeoutTimer, context: context)
356358

357359
switch finalAction {
358360
case .close(let writePromise):

Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ struct HTTP1ConnectionStateMachine {
359359

360360
mutating func idleWriteTimeoutTriggered() -> Action {
361361
guard case .inRequest(var requestStateMachine, let close) = self.state else {
362-
preconditionFailure("Invalid state: \(self.state)")
362+
return .wait
363363
}
364364

365365
return self.avoidingStateMachineCoW { state -> Action in

Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift

+2
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
240240
self.request!.fail(error)
241241
self.request = nil
242242
self.runTimeoutAction(.clearIdleReadTimeoutTimer, context: context)
243+
self.runTimeoutAction(.clearIdleWriteTimeoutTimer, context: context)
243244
// No matter the error reason, we must always make sure the h2 stream is closed. Only
244245
// once the h2 stream is closed, it is released from the h2 multiplexer. The
245246
// HTTPRequestStateMachine may signal finalAction: .none in the error case (as this is
@@ -252,6 +253,7 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
252253
self.request!.succeedRequest(finalParts)
253254
self.request = nil
254255
self.runTimeoutAction(.clearIdleReadTimeoutTimer, context: context)
256+
self.runTimeoutAction(.clearIdleWriteTimeoutTimer, context: context)
255257
self.runSuccessfulFinalAction(finalAction, context: context)
256258

257259
case .failSendBodyPart(let error, let writePromise), .failSendStreamFinished(let error, let writePromise):

Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import NIOSSL
2222
import NIOTLS
2323

2424
#if canImport(Network)
25+
import Network
2526
import NIOTransportServices
2627
#endif
2728

Sources/AsyncHTTPClient/FileDownloadDelegate.swift

+39-2
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,47 @@ import NIOCore
1616
import NIOHTTP1
1717
import NIOPosix
1818

19+
import struct Foundation.URL
20+
1921
/// Handles a streaming download to a given file path, allowing headers and progress to be reported.
2022
public final class FileDownloadDelegate: HTTPClientResponseDelegate {
2123
/// The response type for this delegate: the total count of bytes as reported by the response
22-
/// "Content-Length" header (if available) and the count of bytes downloaded.
24+
/// "Content-Length" header (if available), the count of bytes downloaded, the
25+
/// response head, and a history of requests and responses.
2326
public struct Progress: Sendable {
2427
public var totalBytes: Int?
2528
public var receivedBytes: Int
29+
30+
/// The history of all requests and responses in redirect order.
31+
public var history: [HTTPClient.RequestResponse] = []
32+
33+
/// The target URL (after redirects) of the response.
34+
public var url: URL? {
35+
self.history.last?.request.url
36+
}
37+
38+
public var head: HTTPResponseHead {
39+
get {
40+
assert(self._head != nil)
41+
return self._head!
42+
}
43+
set {
44+
self._head = newValue
45+
}
46+
}
47+
48+
fileprivate var _head: HTTPResponseHead? = nil
49+
50+
internal init(totalBytes: Int? = nil, receivedBytes: Int) {
51+
self.totalBytes = totalBytes
52+
self.receivedBytes = receivedBytes
53+
}
2654
}
2755

28-
private var progress = Progress(totalBytes: nil, receivedBytes: 0)
56+
private var progress = Progress(
57+
totalBytes: nil,
58+
receivedBytes: 0
59+
)
2960

3061
public typealias Response = Progress
3162

@@ -129,10 +160,16 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate {
129160
)
130161
}
131162

163+
public func didVisitURL(task: HTTPClient.Task<Progress>, _ request: HTTPClient.Request, _ head: HTTPResponseHead) {
164+
self.progress.history.append(.init(request: request, responseHead: head))
165+
}
166+
132167
public func didReceiveHead(
133168
task: HTTPClient.Task<Response>,
134169
_ head: HTTPResponseHead
135170
) -> EventLoopFuture<Void> {
171+
self.progress._head = head
172+
136173
self.reportHead?(task, head)
137174

138175
if let totalBytesString = head.headers.first(name: "Content-Length"),

0 commit comments

Comments
 (0)