From 4795f62c774f2bb84ec2c64d568b65f57bb100dc Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 9 Aug 2022 10:55:22 +0100 Subject: [PATCH 1/4] Use Docc for documentation Motivation Documentation is nice, and we can help support users by providing useful clear docs. Modifications Add Docc to 5.6 and later builds Make sure symbol references work Add overview docs Result Nice rendering docs --- Package.swift | 3 +- Package@swift-5.4.swift | 74 ++++ Package@swift-5.5.swift | 74 ++++ .../AsyncAwait/HTTPClientRequest.swift | 82 +++- .../AsyncAwait/HTTPClientResponse.swift | 15 + Sources/AsyncHTTPClient/Docs.docc/index.md | 349 ++++++++++++++++++ .../FileDownloadDelegate.swift | 1 + Sources/AsyncHTTPClient/HTTPClient.swift | 33 +- Sources/AsyncHTTPClient/HTTPHandler.swift | 105 ++++-- .../NIOTransportServices/NWErrorHandler.swift | 4 +- Sources/AsyncHTTPClient/Utils.swift | 4 + 11 files changed, 693 insertions(+), 51 deletions(-) create mode 100644 Package@swift-5.4.swift create mode 100644 Package@swift-5.5.swift create mode 100644 Sources/AsyncHTTPClient/Docs.docc/index.md diff --git a/Package.swift b/Package.swift index a9ea2ba7f..a60f012aa 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.6 //===----------------------------------------------------------------------===// // // This source file is part of the AsyncHTTPClient open source project @@ -28,6 +28,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.11.4"), .package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ], targets: [ .target(name: "CAsyncHTTPClient"), diff --git a/Package@swift-5.4.swift b/Package@swift-5.4.swift new file mode 100644 index 000000000..a9ea2ba7f --- /dev/null +++ b/Package@swift-5.4.swift @@ -0,0 +1,74 @@ +// swift-tools-version:5.4 +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "async-http-client", + products: [ + .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.38.0"), + .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.14.1"), + .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.19.0"), + .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.10.0"), + .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.11.4"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"), + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), + ], + targets: [ + .target(name: "CAsyncHTTPClient"), + .target( + name: "AsyncHTTPClient", + dependencies: [ + .target(name: "CAsyncHTTPClient"), + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), + .product(name: "NIOSOCKS", package: "swift-nio-extras"), + .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), + .product(name: "Logging", package: "swift-log"), + .product(name: "Atomics", package: "swift-atomics"), + ] + ), + .testTarget( + name: "AsyncHTTPClientTests", + dependencies: [ + .target(name: "AsyncHTTPClient"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), + .product(name: "NIOEmbedded", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSOCKS", package: "swift-nio-extras"), + .product(name: "Logging", package: "swift-log"), + .product(name: "Atomics", package: "swift-atomics"), + ], + resources: [ + .copy("Resources/self_signed_cert.pem"), + .copy("Resources/self_signed_key.pem"), + ] + ), + ] +) diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift new file mode 100644 index 000000000..a9ea2ba7f --- /dev/null +++ b/Package@swift-5.5.swift @@ -0,0 +1,74 @@ +// swift-tools-version:5.4 +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "async-http-client", + products: [ + .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.38.0"), + .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.14.1"), + .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.19.0"), + .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.10.0"), + .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.11.4"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"), + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), + ], + targets: [ + .target(name: "CAsyncHTTPClient"), + .target( + name: "AsyncHTTPClient", + dependencies: [ + .target(name: "CAsyncHTTPClient"), + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), + .product(name: "NIOSOCKS", package: "swift-nio-extras"), + .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), + .product(name: "Logging", package: "swift-log"), + .product(name: "Atomics", package: "swift-atomics"), + ] + ), + .testTarget( + name: "AsyncHTTPClientTests", + dependencies: [ + .target(name: "AsyncHTTPClient"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), + .product(name: "NIOEmbedded", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSOCKS", package: "swift-nio-extras"), + .product(name: "Logging", package: "swift-log"), + .product(name: "Atomics", package: "swift-atomics"), + ], + resources: [ + .copy("Resources/self_signed_cert.pem"), + .copy("Resources/self_signed_key.pem"), + ] + ), + ] +) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift index cfab828a0..1d835924d 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift @@ -16,12 +16,21 @@ import NIOCore import NIOHTTP1 +/// A representation of a HTTP request for the Swift Concurrency HTTPClient API. +/// +/// This object is similar to ``HTTPClient/Request``, but used for the Swift Concurrency API. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public struct HTTPClientRequest { + /// The request URL, including scheme and hostname. public var url: String + + /// The request method. public var method: HTTPMethod + + /// The request headers. public var headers: HTTPHeaders + /// The request body, if any. public var body: Body? public init(url: String) { @@ -34,6 +43,10 @@ public struct HTTPClientRequest { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest { + /// A HTTP request body. + /// + /// This object encapsulates the difference between streamed HTTP request bodies and those bodies that + /// are already entirely in memory. public struct Body { @usableFromInline internal enum Mode { @@ -54,10 +67,20 @@ extension HTTPClientRequest { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest.Body { + /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `ByteBuffer`. + /// + /// - parameter byteBuffer: The bytes of the body. public static func bytes(_ byteBuffer: ByteBuffer) -> Self { self.init(.byteBuffer(byteBuffer)) } + /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `RandomAccessCollection` of bytes. + /// + /// This construction will flatten the bytes into a `ByteBuffer`. As a result, the peak memory + /// usage of this construction will be double the size of the original collection. The construction + /// of the `ByteBuffer` will be delayed until it's needed. + /// + /// - parameter bytes: The bytes of the request body. @inlinable public static func bytes( _ bytes: Bytes @@ -75,6 +98,23 @@ extension HTTPClientRequest.Body { }) } + /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `Sequence` of bytes. + /// + /// This construction will flatten the bytes into a `ByteBuffer`. As a result, the peak memory + /// usage of this construction will be double the size of the original collection. The construction + /// of the `ByteBuffer` will be delayed until it's needed. + /// + /// Unlike ``bytes(_:)-1uns7``, this construction does not assume that the body can be replayed. As a result, + /// if a redirect is encountered that would need us to replay the request body, the redirect will instead + /// not be followed. Prefer ``bytes(_:)-1uns7`` wherever possible. + /// + /// Caution should be taken with this method to ensure that the `length` is correct. Incorrect lengths + /// will cause unnecessary runtime failures. Setting `length` to ``Length/unknown`` will trigger the upload + /// to use `chunked` `Transfer-Encoding`, while using ``Length/known(_:)`` will use `Content-Length`. + /// + /// - parameters: + /// - bytes: The bytes of the request body. + /// - length: The length of the request body. @inlinable public static func bytes( _ bytes: Bytes, @@ -93,6 +133,19 @@ extension HTTPClientRequest.Body { }) } + /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `Collection` of bytes. + /// + /// This construction will flatten the bytes into a `ByteBuffer`. As a result, the peak memory + /// usage of this construction will be double the size of the original collection. The construction + /// of the `ByteBuffer` will be delayed until it's needed. + /// + /// Caution should be taken with this method to ensure that the `length` is correct. Incorrect lengths + /// will cause unnecessary runtime failures. Setting `length` to ``Length/unknown`` will trigger the upload + /// to use `chunked` `Transfer-Encoding`, while using ``Length/known(_:)`` will use `Content-Length`. + /// + /// - parameters: + /// - bytes: The bytes of the request body. + /// - length: The length of the request body. @inlinable public static func bytes( _ bytes: Bytes, @@ -111,6 +164,17 @@ extension HTTPClientRequest.Body { }) } + /// Create a ``HTTPClientRequest/Body-swift.struct`` from an `AsyncSequence` of `ByteBuffer`s. + /// + /// This construction will stream the upload one `ByteBuffer` at a time. + /// + /// Caution should be taken with this method to ensure that the `length` is correct. Incorrect lengths + /// will cause unnecessary runtime failures. Setting `length` to ``Length/unknown`` will trigger the upload + /// to use `chunked` `Transfer-Encoding`, while using ``Length/known(_:)`` will use `Content-Length`. + /// + /// - parameters: + /// - sequenceOfBytes: The bytes of the request body. + /// - length: The length of the request body. @inlinable public static func stream( _ sequenceOfBytes: SequenceOfBytes, @@ -123,6 +187,18 @@ extension HTTPClientRequest.Body { return body } + /// Create a ``HTTPClientRequest/Body-swift.struct`` from an `AsyncSequence` of bytes. + /// + /// This construction will consume 1kB chunks from the `Bytes` and send them at once. This optimizes for + /// `AsyncSEquence`s where + /// + /// Caution should be taken with this method to ensure that the `length` is correct. Incorrect lengths + /// will cause unnecessary runtime failures. Setting `length` to ``Length/unknown`` will trigger the upload + /// to use `chunked` `Transfer-Encoding`, while using ``Length/known(_:)`` will use `Content-Length`. + /// + /// - parameters: + /// - bytes: The bytes of the request body. + /// - length: The length of the request body. @inlinable public static func stream( _ bytes: Bytes, @@ -157,10 +233,12 @@ extension Optional where Wrapped == HTTPClientRequest.Body { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest.Body { + /// The length of a HTTP request body. public struct Length { - /// size of the request body is not known before starting the request + /// The size of the request body is not known before starting the request public static let unknown: Self = .init(storage: .unknown) - /// size of the request body is fixed and exactly `count` bytes + + /// The size of the request body is known and exactly `count` bytes public static func known(_ count: Int) -> Self { .init(storage: .known(count)) } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 52f03089b..8cbf8a2f2 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -16,13 +16,28 @@ import NIOCore import NIOHTTP1 +/// A representation of a HTTP response for the Swift Concurrency HTTPClient API. +/// +/// This object is similar to ``HTTPClient/Response``, but used for the Swift Concurrency API. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public struct HTTPClientResponse { + /// The HTTP version on which the response was received. public var version: HTTPVersion + + /// The HTTP status for this response. public var status: HTTPResponseStatus + + /// The HTTP headers of this response. public var headers: HTTPHeaders + + /// The body of this HTTP response. public var body: Body + /// A representation of the response body for a HTTP response. + /// + /// The body is streamed in as an `AsyncSequence` of `ByteBuffer`, where each `ByteBuffer` contains + /// an arbitrarily large chunk of data. The boundaries between `ByteBuffer` objects in the sequence + /// are entirely synthetic and have no semantic meaning. public struct Body { private let bag: Transaction private let reference: ResponseRef diff --git a/Sources/AsyncHTTPClient/Docs.docc/index.md b/Sources/AsyncHTTPClient/Docs.docc/index.md new file mode 100644 index 000000000..acb408684 --- /dev/null +++ b/Sources/AsyncHTTPClient/Docs.docc/index.md @@ -0,0 +1,349 @@ +# ``AsyncHTTPClient`` + +This package provides simple HTTP Client library built on top of SwiftNIO. + +This library provides the following: +- First class support for Swift Concurrency (since version 1.9.0) +- Asynchronous and non-blocking request methods +- Simple follow-redirects (cookie headers are dropped) +- Streaming body download +- TLS support +- Automatic HTTP/2 over HTTPS (since version 1.7.0) +- Cookie parsing (but not storage) + +--- + +**NOTE**: You will need [Xcode 13.2](https://apps.apple.com/gb/app/xcode/id497799835?mt=12) or [Swift 5.5.2](https://swift.org/download/#swift-552) to try out `AsyncHTTPClient`s new async/await APIs. + +--- + +## Getting Started + +#### Adding the dependency + +Add the following entry in your Package.swift to start using HTTPClient: + +```swift +.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0") +``` +and `AsyncHTTPClient` dependency to your target: +```swift +.target(name: "MyApp", dependencies: [.product(name: "AsyncHTTPClient", package: "async-http-client")]), +``` + +#### Request-Response API + +The code snippet below illustrates how to make a simple GET request to a remote server. + +Please note that the example will spawn a new `EventLoopGroup` which will _create fresh threads_ which is a very costly operation. In a real-world application that uses [SwiftNIO](https://github.com/apple/swift-nio) for other parts of your application (for example a web server), please prefer `eventLoopGroupProvider: .shared(myExistingEventLoopGroup)` to share the `EventLoopGroup` used by AsyncHTTPClient with other parts of your application. + +If your application does not use SwiftNIO yet, it is acceptable to use `eventLoopGroupProvider: .createNew` but please make sure to share the returned `HTTPClient` instance throughout your whole application. Do not create a large number of `HTTPClient` instances with `eventLoopGroupProvider: .createNew`, this is very wasteful and might exhaust the resources of your program. + +```swift +import AsyncHTTPClient + +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) + +/// MARK: - Using Swift Concurrency +let request = HTTPClientRequest(url: "https://apple.com/") +let response = try await httpClient.execute(request, timeout: .seconds(30)) +print("HTTP head", response) +if response.status == .ok { + let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB + // handle body +} else { + // handle remote error +} + + +/// MARK: - Using SwiftNIO EventLoopFuture +httpClient.get(url: "https://apple.com/").whenComplete { result in + switch result { + case .failure(let error): + // process error + case .success(let response): + if response.status == .ok { + // handle response + } else { + // handle remote error + } + } +} +``` + +You should always shut down ``HTTPClient`` instances you created using ``HTTPClient/syncShutdown()``. Please note that you must not call ``HTTPClient/syncShutdown()`` before all requests of the HTTP client have finished, or else the in-flight requests will likely fail because their network connections are interrupted. + +### async/await examples + +Examples for the async/await API can be found in the [`Examples` folder](https://github.com/swift-server/async-http-client/tree/main/Examples) in the repository. + +## Usage guide + +The default HTTP Method is `GET`. In case you need to have more control over the method, or you want to add headers or body, use the ``HTTPClientRequest`` struct: + +#### Using Swift Concurrency + +```swift +import AsyncHTTPClient + +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) +do { + var request = HTTPClientRequest(url: "https://apple.com/") + request.method = .POST + request.headers.add(name: "User-Agent", value: "Swift HTTPClient") + request.body = .bytes(ByteBuffer(string: "some data")) + + let response = try await httpClient.execute(request, timeout: .seconds(30)) + if response.status == .ok { + // handle response + } else { + // handle remote error + } +} catch { + // handle error +} +// it's important to shutdown the httpClient after all requests are done, even if one failed +try await httpClient.shutdown() +``` + +#### Using SwiftNIO EventLoopFuture + +```swift +import AsyncHTTPClient + +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) +defer { + try? httpClient.syncShutdown() +} + +var request = try HTTPClient.Request(url: "https://apple.com/", method: .POST) +request.headers.add(name: "User-Agent", value: "Swift HTTPClient") +request.body = .string("some-body") + +httpClient.execute(request: request).whenComplete { result in + switch result { + case .failure(let error): + // process error + case .success(let response): + if response.status == .ok { + // handle response + } else { + // handle remote error + } + } +} +``` + +### Redirects following +Enable follow-redirects behavior using the client configuration: +```swift +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + configuration: HTTPClient.Configuration(followRedirects: true)) +``` + +### Timeouts +Timeouts (connect and read) can also be set using the client configuration: +```swift +let timeout = HTTPClient.Configuration.Timeout(connect: .seconds(1), read: .seconds(1)) +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + configuration: HTTPClient.Configuration(timeout: timeout)) +``` +or on a per-request basis: +```swift +httpClient.execute(request: request, deadline: .now() + .milliseconds(1)) +``` + +### Streaming +When dealing with larger amount of data, it's critical to stream the response body instead of aggregating in-memory. +The following example demonstrates how to count the number of bytes in a streaming response body: + +#### Using Swift Concurrency +```swift +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) +do { + let request = HTTPClientRequest(url: "https://apple.com/") + let response = try await httpClient.execute(request, timeout: .seconds(30)) + print("HTTP head", response) + + // if defined, the content-length headers announces the size of the body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + + var receivedBytes = 0 + // asynchronously iterates over all body fragments + // this loop will automatically propagate backpressure correctly + for try await buffer in response.body { + // for this example, we are just interested in the size of the fragment + receivedBytes += buffer.readableBytes + + if let expectedBytes = expectedBytes { + // if the body size is known, we calculate a progress indicator + let progress = Double(receivedBytes) / Double(expectedBytes) + print("progress: \(Int(progress * 100))%") + } + } + print("did receive \(receivedBytes) bytes") +} catch { + print("request failed:", error) +} +// it is important to shutdown the httpClient after all requests are done, even if one failed +try await httpClient.shutdown() +``` + +#### Using HTTPClientResponseDelegate and SwiftNIO EventLoopFuture + +```swift +import NIOCore +import NIOHTTP1 + +class CountingDelegate: HTTPClientResponseDelegate { + typealias Response = Int + + var count = 0 + + func didSendRequestHead(task: HTTPClient.Task, _ head: HTTPRequestHead) { + // this is executed right after request head was sent, called once + } + + func didSendRequestPart(task: HTTPClient.Task, _ part: IOData) { + // this is executed when request body part is sent, could be called zero or more times + } + + func didSendRequest(task: HTTPClient.Task) { + // this is executed when request is fully sent, called once + } + + func didReceiveHead( + task: HTTPClient.Task, + _ head: HTTPResponseHead + ) -> EventLoopFuture { + // this is executed when we receive HTTP response head part of the request + // (it contains response code and headers), called once in case backpressure + // is needed, all reads will be paused until returned future is resolved + return task.eventLoop.makeSucceededFuture(()) + } + + func didReceiveBodyPart( + task: HTTPClient.Task, + _ buffer: ByteBuffer + ) -> EventLoopFuture { + // this is executed when we receive parts of the response body, could be called zero or more times + count += buffer.readableBytes + // in case backpressure is needed, all reads will be paused until returned future is resolved + return task.eventLoop.makeSucceededFuture(()) + } + + func didFinishRequest(task: HTTPClient.Task) throws -> Int { + // this is called when the request is fully read, called once + // this is where you return a result or throw any errors you require to propagate to the client + return count + } + + func didReceiveError(task: HTTPClient.Task, _ error: Error) { + // this is called when we receive any network-related error, called once + } +} + +let request = try HTTPClient.Request(url: "https://apple.com/") +let delegate = CountingDelegate() + +httpClient.execute(request: request, delegate: delegate).futureResult.whenSuccess { count in + print(count) +} +``` + +### File downloads + +Based on the `HTTPClientResponseDelegate` example above you can build more complex delegates, +the built-in `FileDownloadDelegate` is one of them. It allows streaming the downloaded data +asynchronously, while reporting the download progress at the same time, like in the following +example: + +```swift +let client = HTTPClient(eventLoopGroupProvider: .createNew) +let request = try HTTPClient.Request( + url: "https://swift.org/builds/development/ubuntu1804/latest-build.yml" +) + +let delegate = try FileDownloadDelegate(path: "/tmp/latest-build.yml", reportProgress: { + if let totalBytes = $0.totalBytes { + print("Total bytes count: \(totalBytes)") + } + print("Downloaded \($0.receivedBytes) bytes so far") +}) + +client.execute(request: request, delegate: delegate).futureResult + .whenSuccess { progress in + if let totalBytes = progress.totalBytes { + print("Final total bytes count: \(totalBytes)") + } + print("Downloaded finished with \(progress.receivedBytes) bytes downloaded") + } +``` + +### Unix Domain Socket Paths +Connecting to servers bound to socket paths is easy: +```swift +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) +httpClient.execute( + .GET, + socketPath: "/tmp/myServer.socket", + urlPath: "/path/to/resource" +).whenComplete (...) +``` + +Connecting over TLS to a unix domain socket path is possible as well: +```swift +let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) +httpClient.execute( + .POST, + secureSocketPath: "/tmp/myServer.socket", + urlPath: "/path/to/resource", + body: .string("hello") +).whenComplete (...) +``` + +Direct URLs can easily be constructed to be executed in other scenarios: +```swift +let socketPathBasedURL = URL( + httpURLWithSocketPath: "/tmp/myServer.socket", + uri: "/path/to/resource" +) +let secureSocketPathBasedURL = URL( + httpsURLWithSocketPath: "/tmp/myServer.socket", + uri: "/path/to/resource" +) +``` + +### Disabling HTTP/2 +The exclusive use of HTTP/1 is possible by setting ``HTTPClient/Configuration/httpVersion-swift.property`` to ``HTTPClient/Configuration/HTTPVersion-swift.struct/http1Only`` on the ``HTTPClient/Configuration``: +```swift +var configuration = HTTPClient.Configuration() +configuration.httpVersion = .http1Only +let client = HTTPClient( + eventLoopGroupProvider: .createNew, + configuration: configuration +) +``` + +## Security + +AsyncHTTPClient's security process is documented on [GitHub](https://github.com/swift-server/async-http-client/blob/main/SECURITY.md). + +## Topics + +### HTTPClient + +- ``HTTPClient`` +- ``HTTPClientRequest`` +- ``HTTPClientResponse`` + +### HTTP Client Delegates + +- ``HTTPClientResponseDelegate`` +- ``ResponseAccumulator`` +- ``FileDownloadDelegate`` +- ``HTTPClientCopyingDelegate`` + +### Errors + +- ``HTTPClientError`` diff --git a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift index 6f046dce9..af80c3f08 100644 --- a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift +++ b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift @@ -38,6 +38,7 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate { private var writeFuture: EventLoopFuture? /// Initializes a new file download delegate. + /// /// - parameters: /// - path: Path to a file you'd like to write the download to. /// - pool: A thread pool to use for asynchronous file I/O. diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 094a6d052..791cfc4c2 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -65,6 +65,9 @@ let globalRequestID = ManagedAtomic(0) /// try client.syncShutdown() /// ``` public class HTTPClient { + /// The `EventLoopGroup` in use by this `HTTPClient`. + /// + /// All HTTP transactions will occur on loops owned by this group. public let eventLoopGroup: EventLoopGroup let eventLoopGroupProvider: EventLoopGroupProvider let configuration: Configuration @@ -74,7 +77,7 @@ public class HTTPClient { internal static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) - /// Create an `HTTPClient` with specified `EventLoopGroup` provider and configuration. + /// Create an ``HTTPClient`` with specified `EventLoopGroup` provider and configuration. /// /// - parameters: /// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created. @@ -86,7 +89,7 @@ public class HTTPClient { backgroundActivityLogger: HTTPClient.loggingDisabled) } - /// Create an `HTTPClient` with specified `EventLoopGroup` provider and configuration. + /// Create an ``HTTPClient`` with specified `EventLoopGroup` provider and configuration. /// /// - parameters: /// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created. @@ -180,7 +183,9 @@ public class HTTPClient { } } - /// Shuts down the client and event loop gracefully. This function is clearly an outlier in that it uses a completion + /// Shuts down the client and event loop gracefully. + /// + /// This function is clearly an outlier in that it uses a completion /// callback instead of an EventLoopFuture. The reason for that is that NIO's EventLoopFutures will call back on an event loop. /// The virtue of this function is to shut the event loop down. To work around that we call back on a DispatchQueue /// instead. @@ -623,11 +628,11 @@ public class HTTPClient { return task } - /// `HTTPClient` configuration. + /// ``HTTPClient`` configuration. public struct Configuration { /// TLS configuration, defaults to `TLSConfiguration.makeClientConfiguration()`. public var tlsConfiguration: Optional - /// Enables following 3xx redirects automatically, defaults to `RedirectConfiguration()`. + /// Enables following 3xx redirects automatically. /// /// Following redirects are supported: /// - `301: Moved Permanently` @@ -638,7 +643,8 @@ public class HTTPClient { /// - `307: Temporary Redirect` /// - `308: Permanent Redirect` public var redirectConfiguration: RedirectConfiguration - /// Default client timeout, defaults to no `read` timeout and 10 seconds `connect` timeout. + /// Default client timeout, defaults to no ``Timeout-swift.struct/read`` timeout + /// and 10 seconds ``Timeout-swift.struct/connect`` timeout. public var timeout: Timeout /// Connection pool configuration. public var connectionPool: ConnectionPool @@ -653,10 +659,12 @@ public class HTTPClient { set {} } - /// is set to `.automatic` by default which will use HTTP/2 if run over https and the server supports it, otherwise HTTP/1 + /// What HTTP versions to use. + /// + /// Set to ``HTTPVersion-swift.struct/automatic`` by default which will use HTTP/2 if run over https and the server supports it, otherwise HTTP/1 public var httpVersion: HTTPVersion - /// Whether `HTTPClient` will let Network.framework sit in the `.waiting` state awaiting new network changes, or fail immediately. Defaults to `true`, + /// Whether ``HTTPClient`` will let Network.framework sit in the `.waiting` state awaiting new network changes, or fail immediately. Defaults to `true`, /// which is the recommended setting. Only set this to `false` when attempting to trigger a particular error path. public var networkFrameworkWaitForConnectivity: Bool @@ -755,11 +763,11 @@ public class HTTPClient { public enum EventLoopGroupProvider { /// `EventLoopGroup` will be provided by the user. Owner of this group is responsible for its lifecycle. case shared(EventLoopGroup) - /// `EventLoopGroup` will be created by the client. When `syncShutdown` is called, created `EventLoopGroup` will be shut down as well. + /// `EventLoopGroup` will be created by the client. When ``HTTPClient/syncShutdown()`` is called, the created `EventLoopGroup` will be shut down as well. case createNew } - /// Specifies how the library will treat event loop passed by the user. + /// Specifies how the library will treat the event loop passed by the user. public struct EventLoopPreference { enum Preference { /// Event Loop will be selected by the library. @@ -830,8 +838,7 @@ extension HTTPClient.Configuration { /// Create timeout. /// /// - parameters: - /// - connect: `connect` timeout. Will default to 10 seconds, if no value is - /// provided. See `var connectionCreationTimeout` + /// - connect: `connect` timeout. Will default to 10 seconds, if no value is provided. /// - read: `read` timeout. public init(connect: TimeAmount? = nil, read: TimeAmount? = nil) { self.connect = connect @@ -897,7 +904,7 @@ extension HTTPClient.Configuration { case automatic } - /// we only use HTTP/1, even if the server would supports HTTP/2 + /// We will only use HTTP/1, even if the server would supports HTTP/2 public static let http1Only: Self = .init(configuration: .http1Only) /// HTTP/2 is used if we connect to a server with HTTPS and the server supports HTTP/2, otherwise we use HTTP/1 diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index c1ce39632..11e3f9281 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -20,13 +20,15 @@ import NIOHTTP1 import NIOSSL extension HTTPClient { - /// Represent request body. + /// A request body. public struct Body { - /// Chunk provider. + /// A streaming uploader. + /// + /// ``StreamWriter`` abstracts public struct StreamWriter { let closure: (IOData) -> EventLoopFuture - /// Create new StreamWriter + /// Create new ``HTTPClient/Body/StreamWriter`` /// /// - parameters: /// - closure: function that will be called to write actual bytes to the channel. @@ -43,7 +45,7 @@ extension HTTPClient { } } - /// Body size. if nil,`Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length` + /// Body size. If nil,`Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length` /// header is set with the given `length`. public var length: Int? /// Body chunk provider. @@ -65,7 +67,7 @@ extension HTTPClient { } } - /// Create and stream body using `StreamWriter`. + /// Create and stream body using ``StreamWriter``. /// /// - parameters: /// - length: Body size. If nil, `Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length` @@ -97,7 +99,7 @@ extension HTTPClient { } } - /// Represent HTTP request. + /// Represents a HTTP request. public struct Request { /// Request HTTP method, defaults to `GET`. public let method: HTTPMethod @@ -226,7 +228,7 @@ extension HTTPClient { } } - /// Represent HTTP response. + /// Represents a HTTP response. public struct Response { /// Remote host of the request. public var host: String @@ -272,7 +274,7 @@ extension HTTPClient { } } - /// HTTP authentication + /// HTTP authentication. public struct Authorization: Hashable { private enum Scheme: Hashable { case Basic(String) @@ -285,18 +287,24 @@ extension HTTPClient { self.scheme = scheme } + /// HTTP basic auth. public static func basic(username: String, password: String) -> HTTPClient.Authorization { return .basic(credentials: Base64.encode(bytes: "\(username):\(password)".utf8)) } + /// HTTP basic auth. + /// + /// This version uses the raw string directly. public static func basic(credentials: String) -> HTTPClient.Authorization { return .init(scheme: .Basic(credentials)) } + /// HTTP bearer auth public static func bearer(tokens: String) -> HTTPClient.Authorization { return .init(scheme: .Bearer(tokens)) } + /// The header string for this auth field. public var headerValue: String { switch self.scheme { case .Basic(let credentials): @@ -308,6 +316,10 @@ extension HTTPClient { } } +/// The default ``HTTPClientResponseDelegate``. +/// +/// This ``HTTPClientResponseDelegate`` buffers a complete HTTP response in memory. It does not stream the response body in. +/// The resulting ``Response`` type is ``HTTPClient/Response``. public class ResponseAccumulator: HTTPClientResponseDelegate { public typealias Response = HTTPClient.Response @@ -385,32 +397,34 @@ public class ResponseAccumulator: HTTPClientResponseDelegate { } } -/// `HTTPClientResponseDelegate` allows an implementation to receive notifications about request processing and to control how response parts are processed. +/// ``HTTPClientResponseDelegate`` allows an implementation to receive notifications about request processing and to control how response parts are processed. +/// /// You can implement this protocol if you need fine-grained control over an HTTP request/response, for example, if you want to inspect the response /// headers before deciding whether to accept a response body, or if you want to stream your request body. Pass an instance of your conforming -/// class to the `HTTPClient.execute()` method and this package will call each delegate method appropriately as the request takes place./ +/// class to the ``HTTPClient/execute(request:delegate:eventLoop:deadline:)`` method and this package will call each delegate method appropriately as the request takes place. /// /// ### Backpressure /// -/// A `HTTPClientResponseDelegate` can be used to exert backpressure on the server response. This is achieved by way of the futures returned from -/// `didReceiveHead` and `didReceiveBodyPart`. The following functions are part of the "backpressure system" in the delegate: +/// A ``HTTPClientResponseDelegate`` can be used to exert backpressure on the server response. This is achieved by way of the futures returned from +/// ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd`` and ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v``. +/// The following functions are part of the "backpressure system" in the delegate: /// -/// - `didReceiveHead` -/// - `didReceiveBodyPart` -/// - `didFinishRequest` -/// - `didReceiveError` +/// - ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd`` +/// - ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v`` +/// - ``HTTPClientResponseDelegate/didFinishRequest(task:)`` +/// - ``HTTPClientResponseDelegate/didReceiveError(task:_:)-fhsg`` /// -/// The first three methods are strictly _exclusive_, with that exclusivity managed by the futures returned by `didReceiveHead` and -/// `didReceiveBodyPart`. What this means is that until the returned future is completed, none of these three methods will be called -/// again. This allows delegates to rate limit the server to a capacity it can manage. `didFinishRequest` does not return a future, +/// The first three methods are strictly _exclusive_, with that exclusivity managed by the futures returned by ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd`` and +/// ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v``. What this means is that until the returned future is completed, none of these three methods will be called +/// again. This allows delegates to rate limit the server to a capacity it can manage. ``HTTPClientResponseDelegate/didFinishRequest(task:)`` does not return a future, /// as we are expecting no more data from the server at this time. /// -/// `didReceiveError` is somewhat special: it signals the end of this regime. `didRecieveError` is not exclusive: it may be called at -/// any time, even if a returned future is not yet completed. `didReceiveError` is terminal, meaning that once it has been called none -/// of these four methods will be called again. This can be used as a signal to abandon all outstanding work. +/// ``HTTPClientResponseDelegate/didReceiveError(task:_:)-fhsg`` is somewhat special: it signals the end of this regime. ``HTTPClientResponseDelegate/didReceiveError(task:_:)-fhsg`` +/// is not exclusive: it may be called at any time, even if a returned future is not yet completed. ``HTTPClientResponseDelegate/didReceiveError(task:_:)-fhsg`` is terminal, meaning +/// that once it has been called none of these four methods will be called again. This can be used as a signal to abandon all outstanding work. /// /// - note: This delegate is strongly held by the `HTTPTaskHandler` -/// for the duration of the `Request` processing and will be +/// for the duration of the ``HTTPClient/Request`` processing and will be /// released together with the `HTTPTaskHandler` when channel is closed. /// Users of the library are not required to keep a reference to the /// object that implements this protocol, but may do so if needed. @@ -428,7 +442,7 @@ public protocol HTTPClientResponseDelegate: AnyObject { /// /// - parameters: /// - task: Current request context. - /// - part: Request body `Part`. + /// - part: Request body part. func didSendRequestPart(task: HTTPClient.Task, _ part: IOData) /// Called when the request is fully sent. Will be called once. @@ -451,7 +465,7 @@ public protocol HTTPClientResponseDelegate: AnyObject { /// You must return an `EventLoopFuture` that you complete when you have finished processing the body part. /// You can create an already succeeded future by calling `task.eventLoop.makeSucceededFuture(())`. /// - /// This function will not be called until the future returned by `didReceiveHead` has completed. + /// This function will not be called until the future returned by ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd`` has completed. /// /// This function will not be called for subsequent body parts until the previous future returned by a /// call to this function completes. @@ -464,19 +478,22 @@ public protocol HTTPClientResponseDelegate: AnyObject { /// Called when error was thrown during request execution. Will be called zero or one time only. Request processing will be stopped after that. /// - /// This function may be called at any time: it does not respect the backpressure exerted by `didReceiveHead` and `didReceiveBodyPart`. - /// All outstanding work may be cancelled when this is received. Once called, no further calls will be made to `didReceiveHead`, `didReceiveBodyPart`, - /// or `didFinishRequest`. + /// This function may be called at any time: it does not respect the backpressure exerted by ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd`` + /// and ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v``. + /// All outstanding work may be cancelled when this is received. Once called, no further calls will be made to + /// ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd``, ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v``, + /// or ``HTTPClientResponseDelegate/didFinishRequest(task:)``. /// /// - parameters: /// - task: Current request context. /// - error: Error that occured during response processing. func didReceiveError(task: HTTPClient.Task, _ error: Error) - /// Called when the complete HTTP request is finished. You must return an instance of your `Response` associated type. Will be called once, except if an error occurred. + /// Called when the complete HTTP request is finished. You must return an instance of your ``Response`` associated type. Will be called once, except if an error occurred. /// - /// This function will not be called until all futures returned by `didReceiveHead` and `didReceiveBodyPart` have completed. Once called, - /// no further calls will be made to `didReceiveHead`, `didReceiveBodyPart`, or `didReceiveError`. + /// This function will not be called until all futures returned by ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd`` and ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v`` + /// have completed. Once called, no further calls will be made to ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd``, ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v``, + /// or ``HTTPClientResponseDelegate/didReceiveError(task:_:)-fhsg``. /// /// - parameters: /// - task: Current request context. @@ -485,20 +502,38 @@ public protocol HTTPClientResponseDelegate: AnyObject { } extension HTTPClientResponseDelegate { + /// Default implementation of ``HTTPClientResponseDelegate/didSendRequestHead(task:_:)-6khai``. + /// + /// By default, this does nothing. public func didSendRequestHead(task: HTTPClient.Task, _ head: HTTPRequestHead) {} + /// Default implementation of ``HTTPClientResponseDelegate/didSendRequestPart(task:_:)-4qxap``. + /// + /// By default, this does nothing. public func didSendRequestPart(task: HTTPClient.Task, _ part: IOData) {} + /// Default implementation of ``HTTPClientResponseDelegate/didSendRequest(task:)-3vqgm``. + /// + /// By default, this does nothing. public func didSendRequest(task: HTTPClient.Task) {} + /// Default implementation of ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd``. + /// + /// By default, this does nothing. public func didReceiveHead(task: HTTPClient.Task, _: HTTPResponseHead) -> EventLoopFuture { - return task.eventLoop.makeSucceededFuture(()) + return task.eventLoop.makeSucceededVoidFuture() } + /// Default implementation of ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v``. + /// + /// By default, this does nothing. public func didReceiveBodyPart(task: HTTPClient.Task, _: ByteBuffer) -> EventLoopFuture { - return task.eventLoop.makeSucceededFuture(()) + return task.eventLoop.makeSucceededVoidFuture() } + /// Default implementation of ``HTTPClientResponseDelegate/didReceiveError(task:_:)-fhsg``. + /// + /// By default, this does nothing. public func didReceiveError(task: HTTPClient.Task, _: Error) {} } @@ -560,7 +595,9 @@ protocol HTTPClientTaskDelegate { } extension HTTPClient { - /// Response execution context. Will be created by the library and could be used for obtaining + /// Response execution context. + /// + /// Will be created by the library and could be used for obtaining /// `EventLoopFuture` of the execution or cancellation of the execution. public final class Task { /// The `EventLoop` the delegate will be executed on. diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift index f732c37f4..9796bc2af 100644 --- a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift +++ b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift @@ -21,6 +21,7 @@ import NIOTransportServices extension HTTPClient { #if canImport(Network) + /// A wrapper for `POSIX` errors thrown by `Network.framework`. public struct NWPOSIXError: Error, CustomStringConvertible { /// POSIX error code (enum) public let errorCode: POSIXErrorCode @@ -40,8 +41,9 @@ extension HTTPClient { public var description: String { return self.reason } } + /// A wrapper for TLS errors thrown by `Network.framework`. public struct NWTLSError: Error, CustomStringConvertible { - /// TLS error status. List of TLS errors can be found in + /// TLS error status. List of TLS errors can be found in `` public let status: OSStatus /// actual reason, in human readable form diff --git a/Sources/AsyncHTTPClient/Utils.swift b/Sources/AsyncHTTPClient/Utils.swift index f4154df3d..94278910f 100644 --- a/Sources/AsyncHTTPClient/Utils.swift +++ b/Sources/AsyncHTTPClient/Utils.swift @@ -14,6 +14,10 @@ import NIOCore +/// A ``HTTPClientResponseDelegate`` that wraps a callback. +/// +/// ``HTTPClientCopyingDelegate`` discards most parts of a HTTP response, but streams the body +/// to the `chunkHandler` provided on ``init(chunkHandler:)``. This is mostly useful for testing. public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate { public typealias Response = Void From eda7a2bf043e4ff3c992a9e77b8ca6c8743d5f8a Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 9 Aug 2022 14:43:49 +0100 Subject: [PATCH 2/4] Clean up soundness --- Sources/AsyncHTTPClient/FileDownloadDelegate.swift | 2 +- scripts/soundness.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift index af80c3f08..75f16f52a 100644 --- a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift +++ b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift @@ -38,7 +38,7 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate { private var writeFuture: EventLoopFuture? /// Initializes a new file download delegate. - /// + /// /// - parameters: /// - path: Path to a file you'd like to write the download to. /// - pool: A thread pool to use for asynchronous file I/O. diff --git a/scripts/soundness.sh b/scripts/soundness.sh index da9a91d24..5170ec4f1 100755 --- a/scripts/soundness.sh +++ b/scripts/soundness.sh @@ -72,7 +72,7 @@ for language in swift-or-c bash dtrace; do matching_files=( -name '*' ) case "$language" in swift-or-c) - exceptions=( -name c_nio_http_parser.c -o -name c_nio_http_parser.h -o -name cpp_magic.h -o -name Package.swift -o -name CNIOSHA1.h -o -name c_nio_sha1.c -o -name ifaddrs-android.c -o -name ifaddrs-android.h) + exceptions=( -name c_nio_http_parser.c -o -name c_nio_http_parser.h -o -name cpp_magic.h -o -name Package.swift -o -name CNIOSHA1.h -o -name c_nio_sha1.c -o -name ifaddrs-android.c -o -name ifaddrs-android.h -o -name 'Package@swift*.swift' ) matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) cat > "$tmp" <<"EOF" //===----------------------------------------------------------------------===// From 2e4eaff66e9fca409fd1ad25b6ab75e95227686d Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 9 Aug 2022 14:50:30 +0100 Subject: [PATCH 3/4] Franz code review comments --- .../AsyncAwait/HTTPClientRequest.swift | 21 ++++++++++--------- .../AsyncAwait/HTTPClientResponse.swift | 6 +++--- Sources/AsyncHTTPClient/HTTPClient.swift | 2 +- Sources/AsyncHTTPClient/HTTPHandler.swift | 4 ++-- Sources/AsyncHTTPClient/Utils.swift | 2 +- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift index 1d835924d..a76b9239a 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift @@ -16,12 +16,12 @@ import NIOCore import NIOHTTP1 -/// A representation of a HTTP request for the Swift Concurrency HTTPClient API. +/// A representation of an HTTP request for the Swift Concurrency HTTPClient API. /// /// This object is similar to ``HTTPClient/Request``, but used for the Swift Concurrency API. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public struct HTTPClientRequest { - /// The request URL, including scheme and hostname. + /// The request URL, including scheme, hostname, and optionally port. public var url: String /// The request method. @@ -43,7 +43,7 @@ public struct HTTPClientRequest { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest { - /// A HTTP request body. + /// An HTTP request body. /// /// This object encapsulates the difference between streamed HTTP request bodies and those bodies that /// are already entirely in memory. @@ -67,14 +67,14 @@ extension HTTPClientRequest { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest.Body { - /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `ByteBuffer`. + /// Create an ``HTTPClientRequest/Body-swift.struct`` from a `ByteBuffer`. /// /// - parameter byteBuffer: The bytes of the body. public static func bytes(_ byteBuffer: ByteBuffer) -> Self { self.init(.byteBuffer(byteBuffer)) } - /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `RandomAccessCollection` of bytes. + /// Create an ``HTTPClientRequest/Body-swift.struct`` from a `RandomAccessCollection` of bytes. /// /// This construction will flatten the bytes into a `ByteBuffer`. As a result, the peak memory /// usage of this construction will be double the size of the original collection. The construction @@ -98,7 +98,7 @@ extension HTTPClientRequest.Body { }) } - /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `Sequence` of bytes. + /// Create an ``HTTPClientRequest/Body-swift.struct`` from a `Sequence` of bytes. /// /// This construction will flatten the bytes into a `ByteBuffer`. As a result, the peak memory /// usage of this construction will be double the size of the original collection. The construction @@ -133,7 +133,7 @@ extension HTTPClientRequest.Body { }) } - /// Create a ``HTTPClientRequest/Body-swift.struct`` from a `Collection` of bytes. + /// Create an ``HTTPClientRequest/Body-swift.struct`` from a `Collection` of bytes. /// /// This construction will flatten the bytes into a `ByteBuffer`. As a result, the peak memory /// usage of this construction will be double the size of the original collection. The construction @@ -164,7 +164,7 @@ extension HTTPClientRequest.Body { }) } - /// Create a ``HTTPClientRequest/Body-swift.struct`` from an `AsyncSequence` of `ByteBuffer`s. + /// Create an ``HTTPClientRequest/Body-swift.struct`` from an `AsyncSequence` of `ByteBuffer`s. /// /// This construction will stream the upload one `ByteBuffer` at a time. /// @@ -187,10 +187,11 @@ extension HTTPClientRequest.Body { return body } - /// Create a ``HTTPClientRequest/Body-swift.struct`` from an `AsyncSequence` of bytes. + /// Create an ``HTTPClientRequest/Body-swift.struct`` from an `AsyncSequence` of bytes. /// /// This construction will consume 1kB chunks from the `Bytes` and send them at once. This optimizes for - /// `AsyncSEquence`s where + /// `AsyncSequence`s where larger chunks are buffered up and available without actually suspending, such + /// as those provided by `FileHandle`. /// /// Caution should be taken with this method to ensure that the `length` is correct. Incorrect lengths /// will cause unnecessary runtime failures. Setting `length` to ``Length/unknown`` will trigger the upload diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 8cbf8a2f2..7ccd74530 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -16,7 +16,7 @@ import NIOCore import NIOHTTP1 -/// A representation of a HTTP response for the Swift Concurrency HTTPClient API. +/// A representation of an HTTP response for the Swift Concurrency HTTPClient API. /// /// This object is similar to ``HTTPClient/Response``, but used for the Swift Concurrency API. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @@ -33,9 +33,9 @@ public struct HTTPClientResponse { /// The body of this HTTP response. public var body: Body - /// A representation of the response body for a HTTP response. + /// A representation of the response body for an HTTP response. /// - /// The body is streamed in as an `AsyncSequence` of `ByteBuffer`, where each `ByteBuffer` contains + /// The body is streamed as an `AsyncSequence` of `ByteBuffer`, where each `ByteBuffer` contains /// an arbitrarily large chunk of data. The boundaries between `ByteBuffer` objects in the sequence /// are entirely synthetic and have no semantic meaning. public struct Body { diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 791cfc4c2..3fbdb1366 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -65,7 +65,7 @@ let globalRequestID = ManagedAtomic(0) /// try client.syncShutdown() /// ``` public class HTTPClient { - /// The `EventLoopGroup` in use by this `HTTPClient`. + /// The `EventLoopGroup` in use by this ``HTTPClient``. /// /// All HTTP transactions will occur on loops owned by this group. public let eventLoopGroup: EventLoopGroup diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 11e3f9281..aeef71ba4 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -99,7 +99,7 @@ extension HTTPClient { } } - /// Represents a HTTP request. + /// Represents an HTTP request. public struct Request { /// Request HTTP method, defaults to `GET`. public let method: HTTPMethod @@ -228,7 +228,7 @@ extension HTTPClient { } } - /// Represents a HTTP response. + /// Represents an HTTP response. public struct Response { /// Remote host of the request. public var host: String diff --git a/Sources/AsyncHTTPClient/Utils.swift b/Sources/AsyncHTTPClient/Utils.swift index 94278910f..ed2819fd0 100644 --- a/Sources/AsyncHTTPClient/Utils.swift +++ b/Sources/AsyncHTTPClient/Utils.swift @@ -14,7 +14,7 @@ import NIOCore -/// A ``HTTPClientResponseDelegate`` that wraps a callback. +/// An ``HTTPClientResponseDelegate`` that wraps a callback. /// /// ``HTTPClientCopyingDelegate`` discards most parts of a HTTP response, but streams the body /// to the `chunkHandler` provided on ``init(chunkHandler:)``. This is mostly useful for testing. From 00ecbfdc8e47b6e46e4aa9444c4665fdab680534 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 9 Aug 2022 14:54:27 +0100 Subject: [PATCH 4/4] Enable test discovery --- docker/docker-compose.1804.54.yaml | 1 + docker/docker-compose.2004.55.yaml | 1 + docker/docker-compose.yaml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.1804.54.yaml b/docker/docker-compose.1804.54.yaml index 660429851..dd869549c 100644 --- a/docker/docker-compose.1804.54.yaml +++ b/docker/docker-compose.1804.54.yaml @@ -11,6 +11,7 @@ services: test: image: async-http-client:18.04-5.4 + command: /bin/bash -xcl "swift test --parallel -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}" environment: [] #- SANITIZER_ARG=--sanitize=thread diff --git a/docker/docker-compose.2004.55.yaml b/docker/docker-compose.2004.55.yaml index 4d0a12ee7..71b75c0fe 100644 --- a/docker/docker-compose.2004.55.yaml +++ b/docker/docker-compose.2004.55.yaml @@ -11,6 +11,7 @@ services: test: image: async-http-client:20.04-5.5 + command: /bin/bash -xcl "swift test --parallel -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}" environment: [] #- SANITIZER_ARG=--sanitize=thread diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 6269e953b..75699c60b 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -28,7 +28,7 @@ services: test: <<: *common - command: /bin/bash -xcl "swift test --parallel -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}" + command: /bin/bash -xcl "swift test --parallel -Xswiftc -warnings-as-errors --enable-test-discovery $${SANITIZER_ARG-}" # util