Skip to content

Commit

Permalink
fix(auth): Using a custom Foundation-based HTTPClient for HTTP Reques…
Browse files Browse the repository at this point in the history
…ts (#3582)

---------

Co-authored-by: Sebastian Villena <[email protected]>
  • Loading branch information
thisisabhash and ruisebas authored May 3, 2024
1 parent 8ebd804 commit 89abfcd
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,5 @@ import AWSClientRuntime
public func baseClientEngine(
for configuration: AWSClientConfiguration<some AWSServiceSpecificConfiguration>
) -> 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 = <your custom client engine>
/// #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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down

0 comments on commit 89abfcd

Please sign in to comment.