Skip to content

Commit 89abfcd

Browse files
fix(auth): Using a custom Foundation-based HTTPClient for HTTP Requests (#3582)
--------- Co-authored-by: Sebastian Villena <[email protected]>
1 parent 8ebd804 commit 89abfcd

File tree

5 files changed

+181
-21
lines changed

5 files changed

+181
-21
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import ClientRuntime
10+
11+
extension Foundation.URLRequest {
12+
init(sdkRequest: ClientRuntime.SdkHttpRequest) async throws {
13+
guard let url = sdkRequest.endpoint.url else {
14+
throw FoundationClientEngineError.invalidRequestURL(sdkRequest: sdkRequest)
15+
}
16+
self.init(url: url)
17+
httpMethod = sdkRequest.method.rawValue
18+
19+
for header in sdkRequest.headers.headers {
20+
for value in header.value {
21+
addValue(value, forHTTPHeaderField: header.name)
22+
}
23+
}
24+
25+
httpBody = try await sdkRequest.body.readData()
26+
}
27+
}
28+
29+
extension ClientRuntime.HttpResponse {
30+
private static func headers(
31+
from allHeaderFields: [AnyHashable: Any]
32+
) -> ClientRuntime.Headers {
33+
var headers = Headers()
34+
for header in allHeaderFields {
35+
switch (header.key, header.value) {
36+
case let (key, value) as (String, String):
37+
headers.add(name: key, value: value)
38+
case let (key, values) as (String, [String]):
39+
headers.add(name: key, values: values)
40+
default: continue
41+
}
42+
}
43+
return headers
44+
}
45+
46+
convenience init(httpURLResponse: HTTPURLResponse, data: Data) throws {
47+
let headers = Self.headers(from: httpURLResponse.allHeaderFields)
48+
let body = ByteStream.data(data)
49+
50+
guard let statusCode = HttpStatusCode(rawValue: httpURLResponse.statusCode) else {
51+
// This shouldn't happen, but `HttpStatusCode` only exposes a failable
52+
// `init`. The alternative here is force unwrapping, but we can't
53+
// make the decision to crash here on behalf on consuming applications.
54+
throw FoundationClientEngineError.unexpectedStatusCode(
55+
statusCode: httpURLResponse.statusCode
56+
)
57+
}
58+
self.init(headers: headers, body: body, statusCode: statusCode)
59+
}
60+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import ClientRuntime
10+
import Amplify
11+
12+
@_spi(FoundationClientEngine)
13+
public struct FoundationClientEngine: HTTPClient {
14+
public func send(request: ClientRuntime.SdkHttpRequest) async throws -> ClientRuntime.HttpResponse {
15+
let urlRequest = try await URLRequest(sdkRequest: request)
16+
17+
let (data, response) = try await URLSession.shared.data(for: urlRequest)
18+
guard let httpURLResponse = response as? HTTPURLResponse else {
19+
// This shouldn't be necessary because we're only making HTTP requests.
20+
// `URLResponse` should always be a `HTTPURLResponse`.
21+
// But to refrain from crashing consuming applications, we're throwing here.
22+
throw FoundationClientEngineError.invalidURLResponse(urlRequest: response)
23+
}
24+
25+
let httpResponse = try HttpResponse(
26+
httpURLResponse: httpURLResponse,
27+
data: data
28+
)
29+
30+
return httpResponse
31+
}
32+
33+
public init() {}
34+
35+
/// no-op
36+
func close() async {}
37+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import Amplify
10+
import ClientRuntime
11+
12+
struct FoundationClientEngineError: AmplifyError {
13+
let errorDescription: ErrorDescription
14+
let recoverySuggestion: RecoverySuggestion
15+
let underlyingError: Error?
16+
17+
// protocol requirement
18+
init(
19+
errorDescription: ErrorDescription,
20+
recoverySuggestion: RecoverySuggestion,
21+
error: Error
22+
) {
23+
self.errorDescription = errorDescription
24+
self.recoverySuggestion = recoverySuggestion
25+
self.underlyingError = error
26+
}
27+
}
28+
29+
extension FoundationClientEngineError {
30+
init(
31+
errorDescription: ErrorDescription,
32+
recoverySuggestion: RecoverySuggestion,
33+
error: Error?
34+
) {
35+
self.errorDescription = errorDescription
36+
self.recoverySuggestion = recoverySuggestion
37+
self.underlyingError = error
38+
}
39+
40+
static func invalidRequestURL(sdkRequest: ClientRuntime.SdkHttpRequest) -> Self {
41+
.init(
42+
errorDescription: """
43+
The SdkHttpRequest generated by ClientRuntime doesn't include a valid URL
44+
- \(sdkRequest)
45+
""",
46+
recoverySuggestion: """
47+
Please open an issue at https://github.com/aws-amplify/amplify-swift
48+
with the contents of this error message.
49+
""",
50+
error: nil
51+
)
52+
}
53+
54+
static func invalidURLResponse(urlRequest: URLResponse) -> Self {
55+
.init(
56+
errorDescription: """
57+
The URLResponse received is not an HTTPURLResponse
58+
- \(urlRequest)
59+
""",
60+
recoverySuggestion: """
61+
Please open an issue at https://github.com/aws-amplify/amplify-swift
62+
with the contents of this error message.
63+
""",
64+
error: nil
65+
)
66+
}
67+
68+
static func unexpectedStatusCode(statusCode: Int) -> Self {
69+
.init(
70+
errorDescription: """
71+
The status code received isn't a valid `HttpStatusCode` value.
72+
- status code: \(statusCode)
73+
""",
74+
recoverySuggestion: """
75+
Please open an issue at https://github.com/aws-amplify/amplify-swift
76+
with the contents of this error message.
77+
""",
78+
error: nil
79+
)
80+
}
81+
}

AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,5 @@ import AWSClientRuntime
1313
public func baseClientEngine(
1414
for configuration: AWSClientConfiguration<some AWSServiceSpecificConfiguration>
1515
) -> HTTPClient {
16-
17-
/// An example of how a client engine provided by aws-swift-sdk can be overridden
18-
/// ```
19-
/// let baseClientEngine: HTTPClient
20-
/// #if os(iOS) || os(macOS)
21-
/// // networking goes through default aws sdk engine
22-
/// baseClientEngine = configuration.httpClientEngine
23-
/// #else
24-
/// // The custom client engine from where we want to route requests
25-
/// // FoundationClientEngine() was an example used in 2.26.x and before
26-
/// baseClientEngine = <your custom client engine>
27-
/// #endif
28-
/// return baseClientEngine
29-
/// ```
30-
///
31-
/// Starting aws-sdk-release 0.34.0, base HTTP client has been defaulted to foundation.
32-
/// Hence, amplify doesn't need an override. So return the httpClientEngine present in the configuration.
33-
return configuration.httpClientEngine
34-
35-
16+
return FoundationClientEngine()
3617
}

AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginGetURLIntegrationTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class AWSS3StoragePluginGetURLIntegrationTests: AWSS3StoragePluginTestBase {
2525
_ = try await Amplify.Storage.uploadData(
2626
path: .fromString(key),
2727
data: Data(key.utf8),
28-
options: .init())
28+
options: .init()
29+
).value
2930

3031
let remoteURL = try await Amplify.Storage.getURL(path: .fromString(key))
3132

0 commit comments

Comments
 (0)