From a2b0b6392ab4be09ad3e5049de236c0c2002d9f7 Mon Sep 17 00:00:00 2001 From: Ian Saultz <52051793+atierian@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:09:18 -0400 Subject: [PATCH] split out some files and clean up error handling in foundation client --- .../ClientRuntimeFoundationBridge.swift | 64 ++++++++++++++ .../FoundationHTTPClientEngine+Error.swift | 81 +++++++++++++++++ .../FoundationHTTPClientEngine.swift | 87 ++----------------- 3 files changed, 151 insertions(+), 81 deletions(-) create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationHTTPClientEngine+Error.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..113acb5e16 --- /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 FoundationHTTPClientError.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 FoundationHTTPClientError.unexpectedStatusCode( + statusCode: httpURLResponse.statusCode + ) + } + self.init(headers: headers, body: body, statusCode: statusCode) + } +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationHTTPClientEngine+Error.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationHTTPClientEngine+Error.swift new file mode 100644 index 0000000000..362bcf803c --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationHTTPClientEngine+Error.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 FoundationHTTPClientError: 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 FoundationHTTPClientError { + 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/FoundationHTTPClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationHTTPClientEngine.swift index 5e5bc6835a..03dfc5abb1 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationHTTPClientEngine.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationHTTPClientEngine.swift @@ -9,93 +9,18 @@ import Foundation import ClientRuntime import Amplify -struct InvalidRequestURL: AmplifyError { - let errorDescription: ErrorDescription - let recoverySuggestion: RecoverySuggestion - let underlyingError: Error? - - init(sdkRequest: ClientRuntime.SdkHttpRequest) { - self.errorDescription = """ - The SdkHttpRequest generated by ClientRuntime doesn't include a valid URL - - \(sdkRequest) - """ - self.recoverySuggestion = """ - Please open an issue at https://github.com/aws-amplify/amplify-swift - with the contents of this error message. - """ - self.underlyingError = nil - } - - // Protocol requirement - init( - errorDescription: ErrorDescription, - recoverySuggestion: RecoverySuggestion, - error: Error - ) { - self.errorDescription = errorDescription - self.recoverySuggestion = recoverySuggestion - self.underlyingError = error - } -} - -extension Foundation.URLRequest { - init(sdkRequest: ClientRuntime.SdkHttpRequest) throws { - guard let url = sdkRequest.endpoint.url else { - throw 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) { - let headers = Self.headers(from: httpURLResponse.allHeaderFields) - let body = HttpBody.stream(ByteStream.from(data: data)) - // fix force unwrap - let statusCode = HttpStatusCode(rawValue: httpURLResponse.statusCode)! - self.init(headers: headers, body: body, statusCode: statusCode) - } -} - @_spi(FoundationHTTPClientEngine) public struct FoundationHTTPClient: 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) - // Quinn says it's ok to do this. - // Safely Force Downcasting a URLResponse to an HTTPURLResponse - - // https://developer.apple.com/forums/thread/120099?answerId=372749022#372749022 - let httpURLResponse = response as! HTTPURLResponse + 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 FoundationHTTPClientError.invalidURLResponse(urlRequest: response) + } let httpResponse = HttpResponse( httpURLResponse: httpURLResponse,