Skip to content

Commit

Permalink
feat!: URLSession-based HTTP Client (#636)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbelkins authored and dayaffe committed Jan 11, 2024
1 parent c4d95f6 commit c275855
Show file tree
Hide file tree
Showing 21 changed files with 803 additions and 81 deletions.
25 changes: 16 additions & 9 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,29 @@ jobs:
- macos-13
xcode:
- Xcode_14.0.1
- Xcode_15.0
- Xcode_15.1
destination:
- 'platform=iOS Simulator,OS=16.0,name=iPhone 13'
- 'platform=iOS Simulator,OS=17.0,name=iPhone 15'
- 'platform=iOS Simulator,OS=16.0,name=iPhone 14'
- 'platform=iOS Simulator,OS=17.2,name=iPhone 15'
- 'platform=tvOS Simulator,OS=16.0,name=Apple TV 4K (at 1080p) (2nd generation)'
- 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)'
- 'platform=OS X'
exclude:
# Don't run old macOS with new Xcode
- runner: macos-12
xcode: Xcode_15.0
xcode: Xcode_15.1
# Don't run new macOS with old Xcode
- runner: macos-13
xcode: Xcode_14.0.1
# Don't run old iOS simulator with new Xcode
- destination: 'platform=iOS Simulator,OS=16.0,name=iPhone 13'
xcode: Xcode_15.0
# Don't run new iOS simulator with old Xcode
- destination: 'platform=iOS Simulator,OS=17.0,name=iPhone 15'
# Don't run old iOS/tvOS simulator with new Xcode
- destination: 'platform=iOS Simulator,OS=16.0,name=iPhone 14'
xcode: Xcode_15.1
- destination: 'platform=tvOS Simulator,OS=16.0,name=Apple TV 4K (at 1080p) (2nd generation)'
xcode: Xcode_15.1
# Don't run new iOS/tvOS simulator with old Xcode
- destination: 'platform=iOS Simulator,OS=17.2,name=iPhone 15'
xcode: Xcode_14.0.1
- destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)'
xcode: Xcode_14.0.1
steps:
- name: Checkout smithy-swift
Expand Down Expand Up @@ -89,6 +95,7 @@ jobs:
- name: Build & Run smithy-swift Kotlin Unit Tests
run: ./gradlew build
- name: Build & Run smithy-swift Swift Unit Tests
timeout-minutes: 15
run: |
set -o pipefail && \
NSUnbufferedIO=YES xcodebuild \
Expand Down
24 changes: 13 additions & 11 deletions Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct DefaultSDKRuntimeConfiguration<DefaultSDKRuntimeRetryStrategy: Ret
/// The HTTP client to be used for HTTP connections.
///
/// If none is provided, the AWS CRT HTTP client will be used.
public var httpClientEngine: HttpClientEngine
public var httpClientEngine: HTTPClient

/// The HTTP client configuration.
///
Expand Down Expand Up @@ -72,8 +72,8 @@ public struct DefaultSDKRuntimeConfiguration<DefaultSDKRuntimeRetryStrategy: Ret
self.clientName = clientName
self.encoder = nil
self.decoder = nil
self.httpClientEngine = Self.defaultHttpClientEngine
self.httpClientConfiguration = Self.defaultHttpClientConfiguration
self.httpClientEngine = Self.makeClient(httpClientConfiguration: self.httpClientConfiguration)
self.idempotencyTokenGenerator = Self.defaultIdempotencyTokenGenerator
self.retryStrategyOptions = Self.defaultRetryStrategyOptions
self.logger = Self.defaultLogger(clientName: clientName)
Expand All @@ -85,16 +85,18 @@ public struct DefaultSDKRuntimeConfiguration<DefaultSDKRuntimeRetryStrategy: Ret
// Exposing these as static properties/methods allows them to be used by custom config objects.
public extension DefaultSDKRuntimeConfiguration {

/// The default HTTP client to use when none is configured
/// The default HTTP client for the target platform, configured with the supplied configuration.
///
/// Is the CRT HTTP client.
static var defaultHttpClientEngine: HttpClientEngine { CRTClientEngine() }

/// The default HTTP client with a specified timeout
///
/// Is the CRT HTTP client.
static func httpClientEngineWithTimeout(timeoutMs: UInt32) -> HttpClientEngine {
return CRTClientEngine(config: CRTClientEngineConfig(connectTimeoutMs: timeoutMs))
/// - Parameter httpClientConfiguration: The configuration for the HTTP client.
/// - Returns: The `CRTClientEngine` client on Mac & Linux platforms, returns `URLSessionHttpClient` on non-Mac Apple platforms.
static func makeClient(httpClientConfiguration: HttpClientConfiguration) -> HTTPClient {
#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
return URLSessionHTTPClient(httpClientConfiguration: httpClientConfiguration)
#else
let connectTimeoutMs = httpClientConfiguration.connectTimeout.map { UInt32($0 * 1_000_000) }
let config = CRTClientEngineConfig(connectTimeoutMs: connectTimeoutMs)
return CRTClientEngine(config: config)
#endif
}

/// The HTTP client configuration to use when none is provided.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ extension EventStream {
public struct DefaultMessageDecoderStream<Event: MessageUnmarshallable>: MessageDecoderStream {
public typealias Element = Event

let stream: Stream
let stream: ReadableStream
let messageDecoder: MessageDecoder
let responseDecoder: ResponseDecoder

public init(stream: Stream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) {
public init(stream: ReadableStream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) {
self.stream = stream
self.messageDecoder = messageDecoder
self.responseDecoder = responseDecoder
}

public struct AsyncIterator: AsyncIteratorProtocol {
let stream: Stream
let stream: ReadableStream
let messageDecoder: MessageDecoder
let responseDecoder: ResponseDecoder

init(stream: Stream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) {
init(stream: ReadableStream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) {
self.stream = stream
self.messageDecoder = messageDecoder
self.responseDecoder = responseDecoder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ extension EventStream {
}

public func readToEndAsync() async throws -> ClientRuntime.Data? {
fatalError("readToEndAsync() is not supported by AsyncStream backed streams")
var data = Data()
while let moreData = try await readAsync(upToCount: Int.max) {
data.append(moreData)
}
return data
}

/// Reads up to `count` bytes from the stream asynchronously
Expand Down Expand Up @@ -167,7 +171,11 @@ extension EventStream {
}

/// Closing the stream is a no-op because the underlying async stream is not owned by this stream
public func close() throws {
public func close() {
// no-op
}

public func closeWithError(_ error: Error) {
// no-op
}
}
Expand Down
14 changes: 5 additions & 9 deletions Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Glibc
import Darwin
#endif

public class CRTClientEngine: HttpClientEngine {
public class CRTClientEngine: HTTPClient {
actor SerialExecutor {

/// Stores the common properties of requests that should share a HTTP connection, such that requests
Expand Down Expand Up @@ -150,7 +150,7 @@ public class CRTClientEngine: HttpClientEngine {
self.serialExecutor = SerialExecutor(config: config)
}

public func execute(request: SdkHttpRequest) async throws -> HttpResponse {
public func send(request: SdkHttpRequest) async throws -> HttpResponse {
let connectionMgr = try await serialExecutor.getOrCreateConnectionPool(endpoint: request.endpoint)
let connection = try await connectionMgr.acquireConnection()

Expand Down Expand Up @@ -294,13 +294,9 @@ public class CRTClientEngine: HttpClientEngine {
self.logger.error("Response encountered an error: \(error)")
}

do {
// closing the stream is required to signal to the caller that the response is complete
// and no more data will be received in this stream
try stream.close()
} catch {
self.logger.error("Failed to close stream: \(error)")
}
// closing the stream is required to signal to the caller that the response is complete
// and no more data will be received in this stream
stream.close()
}

requestOptions.http2ManualDataWrites = http2ManualDataWrites
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import struct Foundation.TimeInterval

public class HttpClientConfiguration {
public var protocolType: ProtocolType
// initialize with default headers

/// The timeout for a request, in seconds.
///
/// If none is provided, the client will use default values based on the platform.
public var connectTimeout: TimeInterval?

/// HTTP headers to be submitted with every HTTP request.
///
/// If none is provided, defaults to no extra headers.
public var defaultHeaders: Headers

// add any other properties here you want to give the service operations
// control over to be mapped to the Http Client

public init(protocolType: ProtocolType = .https,
defaultHeaders: Headers = Headers()) {
/// The URL scheme to be used for HTTP requests. Supported values are `http` and `https`.
///
/// If none is provided, the default protocol for the operation will be used
public var protocolType: ProtocolType?

/// Creates a configuration object for a SDK HTTP client.
///
/// Not all configuration settings may be followed by all clients.
/// - Parameters:
/// - connectTimeout: The maximum time to wait for a response without receiving any data.
/// - defaultHeaders: HTTP headers to be included with every HTTP request.
/// Note that certain headers may cause your API request to fail. Defaults to no headers.
/// - protocolType: The HTTP scheme (`http` or `https`) to be used for API requests. Defaults to the operation's standard configuration.
public init(
connectTimeout: TimeInterval? = nil,
protocolType: ProtocolType = .https,
defaultHeaders: Headers = Headers()
) {
self.protocolType = protocolType
self.defaultHeaders = defaultHeaders
self.connectTimeout = connectTimeout
}
}
14 changes: 12 additions & 2 deletions Sources/ClientRuntime/Networking/Http/HttpClientEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
*/
import AwsCommonRuntimeKit

public protocol HttpClientEngine {
func execute(request: SdkHttpRequest) async throws -> HttpResponse
/// The interface for a client that can be used to perform SDK operations over HTTP.
public protocol HTTPClient {

/// Executes an HTTP request to perform an SDK operation.
///
/// The request must be fully formed (i.e. endpoint resolved, signed, etc.) before sending. Modifying the request after signature may
/// result in a rejected request.
///
/// The request body may be in either the form of in-memory data or an asynchronous data stream.
/// - Parameter request: The HTTP request to be performed.
/// - Returns: An HTTP response for the request. Will throw an error if an error is encountered before the HTTP response is received.
func send(request: SdkHttpRequest) async throws -> HttpResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public struct ContentLengthMiddleware<OperationStackOutput>: Middleware {
// Transfer-Encoding can be sent on all Event Streams where length cannot be determined
// or on blob Data Streams where requiresLength is true and unsignedPayload is false
// Only for HTTP/1.1 requests, will be removed in all HTTP/2 requests
input.headers.update(name: "Transfer-Encoding", value: "Chunked")
input.headers.update(name: "Transfer-Encoding", value: "chunked")
} else {
let operation = context.attributes.get(key: AttributeKey<String>(name: "Operation"))
?? "Error getting operation name"
Expand All @@ -49,10 +49,9 @@ public struct ContentLengthMiddleware<OperationStackOutput>: Middleware {
"Missing content-length for SigV4 signing on operation: \(operation)"
throw StreamError.notSupported(errorMessage)
}
default:
case .noStream:
input.headers.update(name: "Content-Length", value: "0")
}

return try await next.handle(context: context, input: input)
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
/// this class will implement Handler per new middleware implementation
public class SdkHttpClient {

let engine: HttpClientEngine
let engine: HTTPClient

public init(engine: HttpClientEngine, config: HttpClientConfiguration) {
public init(engine: HTTPClient, config: HttpClientConfiguration) {
self.engine = engine
}

Expand All @@ -19,20 +19,20 @@ public class SdkHttpClient {
return clientHandler.eraseToAnyHandler()
}

func execute(request: SdkHttpRequest) async throws -> HttpResponse {
return try await engine.execute(request: request)
func send(request: SdkHttpRequest) async throws -> HttpResponse {
return try await engine.send(request: request)
}
}

struct ClientHandler<OperationStackOutput>: Handler {
let engine: HttpClientEngine
private struct ClientHandler<OperationStackOutput>: Handler {
let engine: HTTPClient
func handle(context: HttpContext, input: SdkHttpRequest) async throws -> OperationOutput<OperationStackOutput> {
let httpResponse: HttpResponse

if context.shouldForceH2(), let crtEngine = engine as? CRTClientEngine {
httpResponse = try await crtEngine.executeHTTP2Request(request: input)
} else {
httpResponse = try await engine.execute(request: input)
httpResponse = try await engine.send(request: input)
}

return OperationOutput<OperationStackOutput>(httpResponse: httpResponse)
Expand Down
Loading

0 comments on commit c275855

Please sign in to comment.