From 6fa88d4734e127e8f4da5e73da364496076bc4a2 Mon Sep 17 00:00:00 2001 From: Ian Saultz <52051793+atierian@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:00:19 -0400 Subject: [PATCH] fix(core): add Foundation HTTP client for watchOS / tvOS (#3230) --- .../AWSCognitoAuthPlugin+Configure.swift | 27 ++++++- .../ClientRuntimeFoundationBridge.swift | 64 +++++++++++++++ .../FoundationClientEngine.swift | 37 +++++++++ .../FoundationClientEngineError.swift | 81 +++++++++++++++++++ .../AWSLocationGeoPlugin+Configure.swift | 11 ++- .../PinpointClient+CredentialsProvider.swift | 7 ++ .../Utils/PinpointRequestsRegistry.swift | 5 +- ...WSCloudWatchLoggingSessionController.swift | 9 +++ .../Predictions/AWSPredictionsService.swift | 31 +++++++ .../Service/Storage/AWSS3StorageService.swift | 20 ++++- 10 files changed, 285 insertions(+), 7 deletions(-) create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift index 99895a0d3b..ae80ff67f2 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift @@ -12,6 +12,7 @@ import AWSCognitoIdentity import AWSCognitoIdentityProvider import AWSPluginsCore import ClientRuntime +@_spi(FoundationClientEngine) import AWSPluginsCore extension AWSCognitoAuthPlugin { @@ -92,9 +93,23 @@ extension AWSCognitoAuthPlugin { ) if var httpClientEngineProxy = httpClientEngineProxy { - let sdkEngine = configuration.httpClientEngine - httpClientEngineProxy.target = sdkEngine + let httpClientEngine: HttpClientEngine + #if os(iOS) || os(macOS) + // networking goes through CRT + httpClientEngine = configuration.httpClientEngine + #else + // networking goes through Foundation + httpClientEngine = FoundationClientEngine() + #endif + httpClientEngineProxy.target = httpClientEngine configuration.httpClientEngine = httpClientEngineProxy + } else { + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif } return CognitoIdentityProviderClient(config: configuration) @@ -110,6 +125,14 @@ extension AWSCognitoAuthPlugin { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: identityPoolConfig.region ) + + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif + return CognitoIdentityClient(config: configuration) default: fatalError() diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift new file mode 100644 index 0000000000..b503ed5f3d --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift @@ -0,0 +1,64 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime + +extension Foundation.URLRequest { + init(sdkRequest: ClientRuntime.SdkHttpRequest) throws { + guard let url = sdkRequest.endpoint.url else { + throw FoundationClientEngineError.invalidRequestURL(sdkRequest: sdkRequest) + } + self.init(url: url) + httpMethod = sdkRequest.method.rawValue + + for header in sdkRequest.headers.headers { + for value in header.value { + addValue(value, forHTTPHeaderField: header.name) + } + } + + switch sdkRequest.body { + case .data(let data): httpBody = data + case .stream(let stream): httpBody = stream.toBytes().getData() + case .none: break + } + } +} + +extension ClientRuntime.HttpResponse { + private static func headers( + from allHeaderFields: [AnyHashable: Any] + ) -> ClientRuntime.Headers { + var headers = Headers() + for header in allHeaderFields { + switch (header.key, header.value) { + case let (key, value) as (String, String): + headers.add(name: key, value: value) + case let (key, values) as (String, [String]): + headers.add(name: key, values: values) + default: continue + } + } + return headers + } + + convenience init(httpURLResponse: HTTPURLResponse, data: Data) throws { + let headers = Self.headers(from: httpURLResponse.allHeaderFields) + let body = HttpBody.stream(ByteStream.from(data: data)) + + guard let statusCode = HttpStatusCode(rawValue: httpURLResponse.statusCode) else { + // This shouldn't happen, but `HttpStatusCode` only exposes a failable + // `init`. The alternative here is force unwrapping, but we can't + // make the decision to crash here on behalf on consuming applications. + throw FoundationClientEngineError.unexpectedStatusCode( + statusCode: httpURLResponse.statusCode + ) + } + self.init(headers: headers, body: body, statusCode: statusCode) + } +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift new file mode 100644 index 0000000000..3bda16f9c1 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime +import Amplify + +@_spi(FoundationClientEngine) +public struct FoundationClientEngine: HttpClientEngine { + public func execute(request: ClientRuntime.SdkHttpRequest) async throws -> ClientRuntime.HttpResponse { + let urlRequest = try URLRequest(sdkRequest: request) + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + guard let httpURLResponse = response as? HTTPURLResponse else { + // This shouldn't be necessary because we're only making HTTP requests. + // `URLResponse` should always be a `HTTPURLResponse`. + // But to refrain from crashing consuming applications, we're throwing here. + throw FoundationClientEngineError.invalidURLResponse(urlRequest: response) + } + + let httpResponse = try HttpResponse( + httpURLResponse: httpURLResponse, + data: data + ) + + return httpResponse + } + + public init() {} + + /// no-op + func close() async {} +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift new file mode 100644 index 0000000000..09e6df49ef --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift @@ -0,0 +1,81 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify +import ClientRuntime + +struct FoundationClientEngineError: AmplifyError { + let errorDescription: ErrorDescription + let recoverySuggestion: RecoverySuggestion + let underlyingError: Error? + + // protocol requirement + init( + errorDescription: ErrorDescription, + recoverySuggestion: RecoverySuggestion, + error: Error + ) { + self.errorDescription = errorDescription + self.recoverySuggestion = recoverySuggestion + self.underlyingError = error + } +} + +extension FoundationClientEngineError { + init( + errorDescription: ErrorDescription, + recoverySuggestion: RecoverySuggestion, + error: Error? + ) { + self.errorDescription = errorDescription + self.recoverySuggestion = recoverySuggestion + self.underlyingError = error + } + + static func invalidRequestURL(sdkRequest: ClientRuntime.SdkHttpRequest) -> Self { + .init( + errorDescription: """ + The SdkHttpRequest generated by ClientRuntime doesn't include a valid URL + - \(sdkRequest) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } + + static func invalidURLResponse(urlRequest: URLResponse) -> Self { + .init( + errorDescription: """ + The URLResponse received is not an HTTPURLResponse + - \(urlRequest) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } + + static func unexpectedStatusCode(statusCode: Int) -> Self { + .init( + errorDescription: """ + The status code received isn't a valid `HttpStatusCode` value. + - status code: \(statusCode) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } +} diff --git a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift index be53fc1f45..c210cdd23f 100644 --- a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift +++ b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift @@ -5,10 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +import Foundation import Amplify import AWSPluginsCore -import Foundation - +@_spi(FoundationClientEngine) import AWSPluginsCore import AWSLocation import AWSClientRuntime @@ -35,6 +35,13 @@ extension AWSLocationGeoPlugin { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: region) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + serviceConfiguration.httpClientEngine = FoundationClientEngine() + #endif + let location = LocationClient(config: serviceConfiguration) let locationService = AWSLocationAdapter(location: location) diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift index f3ff0b4f70..447d092b61 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift @@ -8,6 +8,7 @@ import AWSClientRuntime import AWSPluginsCore import AWSPinpoint +@_spi(FoundationClientEngine) import AWSPluginsCore extension PinpointClient { convenience init(region: String, credentialsProvider: CredentialsProvider) throws { @@ -16,6 +17,12 @@ extension PinpointClient { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif PinpointRequestsRegistry.shared.setCustomHttpEngine(on: configuration) self.init(config: configuration) } diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift index 8c0aeba4a3..7bcd61a0a5 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift @@ -20,9 +20,10 @@ import ClientRuntime } nonisolated func setCustomHttpEngine(on configuration: PinpointClient.PinpointClientConfiguration) { - let oldHttpClientEngine = configuration.httpClientEngine + let baseHTTPClientEngine = configuration.httpClientEngine + configuration.httpClientEngine = CustomPinpointHttpClientEngine( - httpClientEngine: oldHttpClientEngine + httpClientEngine: baseHTTPClientEngine ) } diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift index 18d2483ffb..b12e9c134e 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift @@ -6,6 +6,7 @@ // import AWSPluginsCore +@_spi(FoundationClientEngine) import AWSPluginsCore import Amplify import Combine import Foundation @@ -107,6 +108,14 @@ final class AWSCloudWatchLoggingSessionController { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: region ) + + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif + self.client = CloudWatchLogsClient(config: configuration) } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift index ad7341fede..08a3336789 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift @@ -12,6 +12,7 @@ import AWSTextract import AWSComprehend import AWSPolly import AWSPluginsCore +@_spi(FoundationClientEngine) import AWSPluginsCore import Foundation import ClientRuntime import AWSClientRuntime @@ -37,30 +38,60 @@ class AWSPredictionsService { credentialsProvider: credentialsProvider, region: configuration.convert.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + translateClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsTranslateClient = TranslateClient(config: translateClientConfiguration) let pollyClientConfiguration = try PollyClient.PollyClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.convert.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + pollyClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsPollyClient = PollyClient(config: pollyClientConfiguration) let comprehendClientConfiguration = try ComprehendClient.ComprehendClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.convert.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + comprehendClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsComprehendClient = ComprehendClient(config: comprehendClientConfiguration) let rekognitionClientConfiguration = try RekognitionClient.RekognitionClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.identify.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + rekognitionClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsRekognitionClient = RekognitionClient(config: rekognitionClientConfiguration) let textractClientConfiguration = try TextractClient.TextractClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.identify.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + textractClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsTextractClient = TextractClient(config: textractClientConfiguration) let awsTranscribeStreamingAdapter = AWSTranscribeStreamingAdapter( diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift index 415417bb5a..58329ddb84 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift @@ -10,6 +10,8 @@ import Foundation import AWSS3 import Amplify import AWSPluginsCore +@_spi(FoundationClientEngine) import AWSPluginsCore +import ClientRuntime /// - Tag: AWSS3StorageService class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { @@ -61,9 +63,25 @@ class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { credentialsProvider: credentialsProvider, region: region, signingRegion: region) + if var proxy = httpClientEngineProxy { - proxy.target = clientConfig.httpClientEngine + let httpClientEngine: HttpClientEngine + #if os(iOS) || os(macOS) + httpClientEngine = clientConfig.httpClientEngine + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + httpClientEngine = FoundationClientEngine() + #endif + proxy.target = httpClientEngine clientConfig.httpClientEngine = proxy + } else { + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + clientConfig.httpClientEngine = FoundationClientEngine() + #endif } let s3Client = S3Client(config: clientConfig)