From 89abfcd0f03fa68b748d0db0a1e0ccd2a1bcac91 Mon Sep 17 00:00:00 2001 From: Abhash Kumar Singh Date: Fri, 3 May 2024 13:29:15 -0700 Subject: [PATCH] fix(auth): Using a custom Foundation-based HTTPClient for HTTP Requests (#3582) --------- Co-authored-by: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> --- .../ClientRuntimeFoundationBridge.swift | 60 ++++++++++++++ .../FoundationClientEngine.swift | 37 +++++++++ .../FoundationClientEngineError.swift | 81 +++++++++++++++++++ .../PluginClientEngine.swift | 21 +---- ...3StoragePluginGetURLIntegrationTests.swift | 3 +- 5 files changed, 181 insertions(+), 21 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/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift new file mode 100644 index 0000000000..18fa1470b2 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift @@ -0,0 +1,60 @@ +// +// 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) async 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) + } + } + + httpBody = try await sdkRequest.body.readData() + } +} + +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 = ByteStream.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..d8f18fe889 --- /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: HTTPClient { + public func send(request: ClientRuntime.SdkHttpRequest) async throws -> ClientRuntime.HttpResponse { + let urlRequest = try await 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/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift index cb25e6dc8e..6a2e8d7544 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift @@ -13,24 +13,5 @@ import AWSClientRuntime public func baseClientEngine( for configuration: AWSClientConfiguration ) -> HTTPClient { - - /// An example of how a client engine provided by aws-swift-sdk can be overridden - /// ``` - /// let baseClientEngine: HTTPClient - /// #if os(iOS) || os(macOS) - /// // networking goes through default aws sdk engine - /// baseClientEngine = configuration.httpClientEngine - /// #else - /// // The custom client engine from where we want to route requests - /// // FoundationClientEngine() was an example used in 2.26.x and before - /// baseClientEngine = - /// #endif - /// return baseClientEngine - /// ``` - /// - /// Starting aws-sdk-release 0.34.0, base HTTP client has been defaulted to foundation. - /// Hence, amplify doesn't need an override. So return the httpClientEngine present in the configuration. - return configuration.httpClientEngine - - + return FoundationClientEngine() } diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift index d8a4496e82..f1170140f7 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift @@ -25,7 +25,8 @@ class AWSS3StoragePluginGetURLIntegrationTests: AWSS3StoragePluginTestBase { _ = try await Amplify.Storage.uploadData( path: .fromString(key), data: Data(key.utf8), - options: .init()) + options: .init() + ).value let remoteURL = try await Amplify.Storage.getURL(path: .fromString(key))