mirror of
https://github.com/swift-server/swift-openapi-async-http-client.git
synced 2026-05-03 07:42:29 +00:00
caa53033fe
### Motivation On main, the HTTPBody length type changed from Int to Int64. ### Modifications Adapted the transport with this change. ### Result Repo builds again when using the latest runtime. ### Test Plan Adapted tests.
253 lines
9.7 KiB
Swift
253 lines
9.7 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftOpenAPIGenerator open source project
|
|
//
|
|
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
import OpenAPIRuntime
|
|
import AsyncHTTPClient
|
|
import NIOCore
|
|
import NIOHTTP1
|
|
import NIOFoundationCompat
|
|
import HTTPTypes
|
|
#if canImport(Darwin)
|
|
import Foundation
|
|
#else
|
|
@preconcurrency import struct Foundation.URL
|
|
import struct Foundation.URLComponents
|
|
import struct Foundation.Data
|
|
import protocol Foundation.LocalizedError
|
|
#endif
|
|
|
|
/// A client transport that performs HTTP operations using the HTTPClient type
|
|
/// provided by the AsyncHTTPClient library.
|
|
///
|
|
/// ### Use the AsyncHTTPClient transport
|
|
///
|
|
/// Instantiate the transport:
|
|
///
|
|
/// let transport = AsyncHTTPClientTransport()
|
|
///
|
|
/// Create the base URL of the server to call using your client. If the server
|
|
/// URL was defined in the OpenAPI document, you find a generated method for it
|
|
/// on the `Servers` type, for example:
|
|
///
|
|
/// let serverURL = try Servers.server1()
|
|
///
|
|
/// Instantiate the `Client` type generated by the Swift OpenAPI Generator for
|
|
/// your provided OpenAPI document. For example:
|
|
///
|
|
/// let client = Client(
|
|
/// serverURL: serverURL,
|
|
/// transport: transport
|
|
/// )
|
|
///
|
|
/// Use the client to make HTTP calls defined in your OpenAPI document. For
|
|
/// example, if the OpenAPI document contains an HTTP operation with
|
|
/// the identifier `checkHealth`, call it from Swift with:
|
|
///
|
|
/// let response = try await client.checkHealth(.init())
|
|
/// // ...
|
|
///
|
|
/// ### Provide a custom Client
|
|
///
|
|
/// The ``AsyncHTTPClientTransport/Configuration-swift.struct`` type allows you
|
|
/// to provide a custom `HTTPClient` and tweak behaviors such as the default
|
|
/// timeout.
|
|
public struct AsyncHTTPClientTransport: ClientTransport {
|
|
|
|
/// A set of configuration values for the AsyncHTTPClient transport.
|
|
public struct Configuration: Sendable {
|
|
|
|
/// The HTTP client used for performing HTTP calls.
|
|
public var client: HTTPClient
|
|
|
|
/// The default shared HTTP client.
|
|
///
|
|
/// This is a workaround for the lack of a shared client
|
|
/// in AsyncHTTPClient. Do not use this value directly, outside of
|
|
/// the `Configuration.init(client:timeout:)` initializer, as it will
|
|
/// likely be removed in the future.
|
|
private static let sharedClient: HTTPClient = .init()
|
|
|
|
/// The default request timeout.
|
|
public var timeout: TimeAmount
|
|
|
|
/// Creates a new configuration with the specified client and timeout.
|
|
/// - Parameters:
|
|
/// - client: The underlying client used to perform HTTP operations.
|
|
/// Provide nil to use the shared internal client.
|
|
/// - timeout: The request timeout, defaults to 1 minute.
|
|
public init(client: HTTPClient? = nil, timeout: TimeAmount = .minutes(1)) {
|
|
self.client = client ?? Self.sharedClient
|
|
self.timeout = timeout
|
|
}
|
|
}
|
|
|
|
/// A request to be sent by the transport.
|
|
internal typealias Request = HTTPClientRequest
|
|
|
|
/// A response returned by the transport.
|
|
internal typealias Response = HTTPClientResponse
|
|
|
|
/// Specialized error thrown by the transport.
|
|
internal enum Error: Swift.Error, CustomStringConvertible, LocalizedError {
|
|
|
|
/// Invalid URL composed from base URL and received request.
|
|
case invalidRequestURL(request: HTTPRequest, baseURL: URL)
|
|
|
|
// MARK: CustomStringConvertible
|
|
|
|
var description: String {
|
|
switch self {
|
|
case let .invalidRequestURL(request: request, baseURL: baseURL):
|
|
return
|
|
"Invalid request URL from request path: \(request.path ?? "<nil>") relative to base URL: \(baseURL.absoluteString)"
|
|
}
|
|
}
|
|
|
|
// MARK: LocalizedError
|
|
|
|
var errorDescription: String? { description }
|
|
}
|
|
|
|
/// A set of configuration values used by the transport.
|
|
public var configuration: Configuration
|
|
|
|
/// Underlying request sender for the transport.
|
|
internal let requestSender: any HTTPRequestSending
|
|
|
|
/// Creates a new transport.
|
|
/// - Parameters:
|
|
/// - configuration: A set of configuration values used by the transport.
|
|
/// - requestSender: The underlying request sender.
|
|
internal init(configuration: Configuration, requestSender: any HTTPRequestSending) {
|
|
self.configuration = configuration
|
|
self.requestSender = requestSender
|
|
}
|
|
|
|
/// Creates a new transport.
|
|
/// - Parameter configuration: A set of configuration values used by the transport.
|
|
public init(configuration: Configuration) {
|
|
self.init(configuration: configuration, requestSender: AsyncHTTPRequestSender())
|
|
}
|
|
|
|
// MARK: ClientTransport
|
|
|
|
/// Sends an HTTP request and returns the corresponding HTTP response.
|
|
///
|
|
/// - Parameters:
|
|
/// - request: The HTTP request to send.
|
|
/// - body: The HTTP body to include in the request (optional).
|
|
/// - baseURL: The base URL for the request.
|
|
/// - operationID: The identifier for the operation.
|
|
///
|
|
/// - Returns: A tuple containing the HTTP response and an optional HTTP body in the response.
|
|
/// - Throws: An error if the request or response handling encounters any issues.
|
|
public func send(_ request: HTTPRequest, body: HTTPBody?, baseURL: URL, operationID: String) async throws -> (
|
|
HTTPResponse, HTTPBody?
|
|
) {
|
|
let httpRequest = try Self.convertRequest(request, body: body, baseURL: baseURL)
|
|
let httpResponse = try await invokeSession(with: httpRequest)
|
|
let response = try await Self.convertResponse(method: request.method, httpResponse: httpResponse)
|
|
return response
|
|
}
|
|
|
|
// MARK: Internal
|
|
|
|
/// Converts the shared Request type into URLRequest.
|
|
internal static func convertRequest(_ request: HTTPRequest, body: HTTPBody?, baseURL: URL) throws
|
|
-> HTTPClientRequest
|
|
{
|
|
guard var baseUrlComponents = URLComponents(string: baseURL.absoluteString),
|
|
let requestUrlComponents = URLComponents(string: request.path ?? "")
|
|
else { throw Error.invalidRequestURL(request: request, baseURL: baseURL) }
|
|
baseUrlComponents.percentEncodedPath += requestUrlComponents.percentEncodedPath
|
|
baseUrlComponents.percentEncodedQuery = requestUrlComponents.percentEncodedQuery
|
|
guard let url = baseUrlComponents.url else { throw Error.invalidRequestURL(request: request, baseURL: baseURL) }
|
|
var clientRequest = HTTPClientRequest(url: url.absoluteString)
|
|
clientRequest.method = request.method.asHTTPMethod
|
|
for header in request.headerFields {
|
|
clientRequest.headers.add(name: header.name.canonicalName, value: header.value)
|
|
}
|
|
if let body {
|
|
let length: HTTPClientRequest.Body.Length
|
|
switch body.length {
|
|
case .unknown: length = .unknown
|
|
case .known(let count): length = .known(Int(count))
|
|
}
|
|
clientRequest.body = .stream(body.map { .init(bytes: $0) }, length: length)
|
|
}
|
|
return clientRequest
|
|
}
|
|
|
|
/// Converts the received URLResponse into the shared Response.
|
|
internal static func convertResponse(method: HTTPRequest.Method, httpResponse: HTTPClientResponse) async throws -> (
|
|
HTTPResponse, HTTPBody?
|
|
) {
|
|
|
|
var headerFields: HTTPFields = [:]
|
|
for header in httpResponse.headers { headerFields[.init(header.name)!] = header.value }
|
|
|
|
let length: HTTPBody.Length
|
|
if let lengthHeaderString = headerFields[.contentLength], let lengthHeader = Int64(lengthHeaderString) {
|
|
length = .known(lengthHeader)
|
|
} else {
|
|
length = .unknown
|
|
}
|
|
|
|
let body: HTTPBody?
|
|
switch method {
|
|
case .head, .connect, .trace: body = nil
|
|
default:
|
|
body = HTTPBody(httpResponse.body.map { $0.readableBytesView }, length: length, iterationBehavior: .single)
|
|
}
|
|
|
|
let response = HTTPResponse(status: .init(code: Int(httpResponse.status.code)), headerFields: headerFields)
|
|
return (response, body)
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
/// Makes the underlying HTTP call.
|
|
private func invokeSession(with request: Request) async throws -> Response {
|
|
try await requestSender.send(request: request, with: configuration.client, timeout: configuration.timeout)
|
|
}
|
|
}
|
|
|
|
extension HTTPTypes.HTTPRequest.Method {
|
|
var asHTTPMethod: NIOHTTP1.HTTPMethod {
|
|
switch self {
|
|
case .get: return .GET
|
|
case .put: return .PUT
|
|
case .post: return .POST
|
|
case .delete: return .DELETE
|
|
case .options: return .OPTIONS
|
|
case .head: return .HEAD
|
|
case .patch: return .PATCH
|
|
case .trace: return .TRACE
|
|
default: return .RAW(value: rawValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A type that performs HTTP operations using the HTTP client.
|
|
internal protocol HTTPRequestSending: Sendable {
|
|
func send(request: AsyncHTTPClientTransport.Request, with client: HTTPClient, timeout: TimeAmount) async throws
|
|
-> AsyncHTTPClientTransport.Response
|
|
}
|
|
|
|
/// Performs HTTP calls using AsyncHTTPClient
|
|
internal struct AsyncHTTPRequestSender: HTTPRequestSending {
|
|
func send(request: AsyncHTTPClientTransport.Request, with client: AsyncHTTPClient.HTTPClient, timeout: TimeAmount)
|
|
async throws -> AsyncHTTPClientTransport.Response
|
|
{ try await client.execute(request, timeout: timeout) }
|
|
}
|