Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(auth): use custom HTTPClient for HTTP Requests #3582

Merged
merged 3 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading