Skip to content

Commit 3adac87

Browse files
lawmichapalpatim
andauthored
fix(API): Return response body for non-2xx failure cases (#1076)
* fix(API): Return response body for non-2xx failure cases * fix(API): PR feedback * fix(API): PR feedback - fix NSCoder logic * Update AmplifyPlugins/API/AWSAPICategoryPlugin/Operation/AWSHTTPURLResponse.swift Co-authored-by: Tim Schmelter <[email protected]> Co-authored-by: Tim Schmelter <[email protected]>
1 parent 80861a0 commit 3adac87

File tree

7 files changed

+190
-22
lines changed

7 files changed

+190
-22
lines changed

AmplifyPlugins/API/APICategoryPlugin.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/* Begin PBXBuildFile section */
1010
13066F229854831AA628ECEC /* Pods_HostApp_AWSAPICategoryPluginTestCommon_RESTWithUserPoolIntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DD6386039136045F18D44AC /* Pods_HostApp_AWSAPICategoryPluginTestCommon_RESTWithUserPoolIntegrationTests.framework */; };
11+
211BEDF025E5904E004F367A /* AWSHTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 211BEDEF25E5904E004F367A /* AWSHTTPURLResponse.swift */; };
12+
211BEDFB25E59DC8004F367A /* AWSHTTPURLResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 211BEDFA25E59DC8004F367A /* AWSHTTPURLResponseTests.swift */; };
1113
21233D032469B06B00039337 /* SocialNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21233D022469B06B00039337 /* SocialNote.swift */; };
1214
21233D052469DF1200039337 /* GraphQLAuthDirectiveIntegrationTests+Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21233D042469DF1200039337 /* GraphQLAuthDirectiveIntegrationTests+Support.swift */; };
1315
21233D092469EBA000039337 /* GraphQLAuthDirectiveIntegrationTests-credentials.json in Resources */ = {isa = PBXBuildFile; fileRef = 21233D062469EBA000039337 /* GraphQLAuthDirectiveIntegrationTests-credentials.json */; };
@@ -24,7 +26,6 @@
2426
214BD3B223FB5C4C0059A286 /* SubscriptionConnectionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214BD38B23FB5C4C0059A286 /* SubscriptionConnectionFactory.swift */; };
2527
214BD3D023FB76740059A286 /* AWSSubscriptionConnectionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214BD3CF23FB76740059A286 /* AWSSubscriptionConnectionFactory.swift */; };
2628
21598CE3239FF54E00529F29 /* RESTWithIAMIntegrationTests-amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 21598CDD239EDF1000529F29 /* RESTWithIAMIntegrationTests-amplifyconfiguration.json */; };
27-
21598CE4239FF61300529F29 /* RESTWithIAMIntegrationTests-awsconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 21598CDE239EDF1000529F29 /* RESTWithIAMIntegrationTests-awsconfiguration.json */; };
2829
21598CF223A0164A00529F29 /* GraphQLWithUserPoolIntegrationTests-amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 21598CF023A0164900529F29 /* GraphQLWithUserPoolIntegrationTests-amplifyconfiguration.json */; };
2930
21598CF323A0164A00529F29 /* GraphQLWithUserPoolIntegrationTests-awsconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 21598CF123A0164A00529F29 /* GraphQLWithUserPoolIntegrationTests-awsconfiguration.json */; };
3031
216201E323920A6200AB2E10 /* GraphQLSyncBasedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216201E223920A6200AB2E10 /* GraphQLSyncBasedTests.swift */; };
@@ -302,6 +303,8 @@
302303
1B13CFC866A30622EDD91AF4 /* Pods_AWSAPICategoryPlugin_AWSAPICategoryPluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAPICategoryPlugin_AWSAPICategoryPluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
303304
1B30959CE873C097E54BDFD6 /* Pods-GraphQLWithAPIKeyIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GraphQLWithAPIKeyIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-GraphQLWithAPIKeyIntegrationTests/Pods-GraphQLWithAPIKeyIntegrationTests.release.xcconfig"; sourceTree = "<group>"; };
304305
1FDC07084023DEF205FF6598 /* Pods-AWSAPICategoryPlugin-AWSAPICategoryPluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AWSAPICategoryPlugin-AWSAPICategoryPluginTests.debug.xcconfig"; path = "Target Support Files/Pods-AWSAPICategoryPlugin-AWSAPICategoryPluginTests/Pods-AWSAPICategoryPlugin-AWSAPICategoryPluginTests.debug.xcconfig"; sourceTree = "<group>"; };
306+
211BEDEF25E5904E004F367A /* AWSHTTPURLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSHTTPURLResponse.swift; sourceTree = "<group>"; };
307+
211BEDFA25E59DC8004F367A /* AWSHTTPURLResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSHTTPURLResponseTests.swift; sourceTree = "<group>"; };
305308
21233D022469B06B00039337 /* SocialNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialNote.swift; sourceTree = "<group>"; };
306309
21233D042469DF1200039337 /* GraphQLAuthDirectiveIntegrationTests+Support.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraphQLAuthDirectiveIntegrationTests+Support.swift"; sourceTree = "<group>"; };
307310
21233D062469EBA000039337 /* GraphQLAuthDirectiveIntegrationTests-credentials.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "GraphQLAuthDirectiveIntegrationTests-credentials.json"; sourceTree = "<group>"; };
@@ -320,7 +323,6 @@
320323
214BD38B23FB5C4C0059A286 /* SubscriptionConnectionFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionConnectionFactory.swift; sourceTree = "<group>"; };
321324
214BD3CF23FB76740059A286 /* AWSSubscriptionConnectionFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AWSSubscriptionConnectionFactory.swift; sourceTree = "<group>"; };
322325
21598CDD239EDF1000529F29 /* RESTWithIAMIntegrationTests-amplifyconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "RESTWithIAMIntegrationTests-amplifyconfiguration.json"; sourceTree = "<group>"; };
323-
21598CDE239EDF1000529F29 /* RESTWithIAMIntegrationTests-awsconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "RESTWithIAMIntegrationTests-awsconfiguration.json"; sourceTree = "<group>"; };
324326
21598CE5239FF8BD00529F29 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
325327
21598CE723A0036600529F29 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
326328
21598CE823A0037800529F29 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@@ -795,7 +797,6 @@
795797
217856842380C90600A30D19 /* Info.plist */,
796798
21598CE5239FF8BD00529F29 /* README.md */,
797799
21598CDD239EDF1000529F29 /* RESTWithIAMIntegrationTests-amplifyconfiguration.json */,
798-
21598CDE239EDF1000529F29 /* RESTWithIAMIntegrationTests-awsconfiguration.json */,
799800
217856822380C90600A30D19 /* RESTWithIAMIntegrationTests.swift */,
800801
);
801802
path = RESTWithIAMIntegrationTests;
@@ -883,6 +884,7 @@
883884
21D7A091237B54D90057D00D /* AWSGraphQLOperation+APIOperation.swift */,
884885
21D7A08E237B54D90057D00D /* AWSGraphQLSubscriptionOperation.swift */,
885886
21D7A08F237B54D90057D00D /* AWSRESTOperation.swift */,
887+
211BEDEF25E5904E004F367A /* AWSHTTPURLResponse.swift */,
886888
);
887889
path = Operation;
888890
sourceTree = "<group>";
@@ -1133,6 +1135,7 @@
11331135
FAF7066F24C8EFD300F19DCF /* GraphQLSubscribeTests.swift */,
11341136
FAF2199924C0F25E00171A3D /* OperationTestBase.swift */,
11351137
FAA7A5A724C0DE1900CA863F /* RESTCombineTests.swift */,
1138+
211BEDFA25E59DC8004F367A /* AWSHTTPURLResponseTests.swift */,
11361139
);
11371140
path = Operation;
11381141
sourceTree = "<group>";
@@ -1618,7 +1621,6 @@
16181621
21598CF323A0164A00529F29 /* GraphQLWithUserPoolIntegrationTests-awsconfiguration.json in Resources */,
16191622
21233D092469EBA000039337 /* GraphQLAuthDirectiveIntegrationTests-credentials.json in Resources */,
16201623
B478F6E22374E0CF00C4F92B /* LaunchScreen.storyboard in Resources */,
1621-
21598CE4239FF61300529F29 /* RESTWithIAMIntegrationTests-awsconfiguration.json in Resources */,
16221624
);
16231625
runOnlyForDeploymentPostprocessing = 0;
16241626
};
@@ -2333,6 +2335,7 @@
23332335
21D7A11A237B54D90057D00D /* AWSAPICategoryPluginError.swift in Sources */,
23342336
21D7A117237B54D90057D00D /* UserPoolRequestInterceptor.swift in Sources */,
23352337
21D38B9B240C517C00EC2A8D /* AWSOIDCAuthProvider.swift in Sources */,
2338+
211BEDF025E5904E004F367A /* AWSHTTPURLResponse.swift in Sources */,
23362339
216449472587E92B00C548A5 /* GraphQLResponseDecoder+DecodeData.swift in Sources */,
23372340
21A4EEF8259E38B500E1047D /* AppSyncListDecoder.swift in Sources */,
23382341
21D7A112237B54D90057D00D /* GraphQLOperationRequestUtils+Validator.swift in Sources */,
@@ -2395,6 +2398,7 @@
23952398
6B2E465A23AAA6AF0066EDCE /* NetworkReachabilityNotifierTests.swift in Sources */,
23962399
B4DFA5E1237A611D0013E17B /* MockURLSessionTask.swift in Sources */,
23972400
FAA7A5AA24C0E01500CA863F /* AWSGraphQLSubscriptionOperationCancelTests.swift in Sources */,
2401+
211BEDFB25E59DC8004F367A /* AWSHTTPURLResponseTests.swift in Sources */,
23982402
B4DFA5F8237A611D0013E17B /* AWSAPICategoryPlugin+ConfigureTests.swift in Sources */,
23992403
B4DFA5F1237A611D0013E17B /* AWSAPICategoryPlugin+URLSessionBehaviorDelegateTests.swift in Sources */,
24002404
B4DFA5E7237A611D0013E17B /* AWSAPICategoryPlugin+InterceptorBehaviorTests.swift in Sources */,

AmplifyPlugins/API/AWSAPICategoryPlugin/Operation/APIOperationResponse.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import Amplify
1111
struct APIOperationResponse {
1212
let urlError: URLError?
1313
let httpURLResponse: HTTPURLResponse?
14+
let responseData: Data?
1415

15-
public init(error: Error?, response: URLResponse?) {
16+
public init(error: Error?, response: URLResponse?, data: Data? = nil) {
1617
self.urlError = error as? URLError
1718
self.httpURLResponse = response as? HTTPURLResponse
19+
self.responseData = data
1820
}
1921
}
2022

@@ -32,7 +34,11 @@ extension APIOperationResponse {
3234

3335
let successStatusCodes = 200 ..< 300
3436
if !successStatusCodes.contains(statusCode) {
35-
throw APIError.httpStatusError(statusCode, response)
37+
if let restResponse = AWSHTTPURLResponse(response: response, body: responseData) {
38+
throw APIError.httpStatusError(statusCode, restResponse)
39+
} else {
40+
throw APIError.httpStatusError(statusCode, response)
41+
}
3642
}
3743
case (.some(let error), .some(let response)):
3844
let userInfo = ["HTTPURLResponse": response]

AmplifyPlugins/API/AWSAPICategoryPlugin/Operation/AWSAPIOperation+APIOperation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extension AWSRESTOperation: APIOperation {
2323
return
2424
}
2525

26-
let apiOperationResponse = APIOperationResponse(error: nil, response: response)
26+
let apiOperationResponse = APIOperationResponse(error: nil, response: response, data: data)
2727
do {
2828
try apiOperationResponse.validate()
2929
} catch let error as APIError {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
10+
/// `AWSHTTPURLResponse` contains the response body and metadata associated with the response to an HTTP request.
11+
///
12+
/// When using `AWSAPIPlugin`, you can optionally type cast `HTTPURLResponse` to an instances of `AWSHTTPURLResponse`.
13+
/// The response body can be accessed from the `body: Data?` property. For example, when the `APIError` is an
14+
/// `.httpStatusError(StatusCode, HTTPURLResponse)`, then access the response body by type casting the response to
15+
/// an `AWSHTTPURLResponse` and retrieve the `body` field.
16+
/// ```swift
17+
/// if case let .httpStatusError(statusCode, response) = error, let awsResponse = response as? AWSHTTPURLResponse {
18+
/// if let responseBody = awsResponse.body {
19+
/// print("Response contains a \(responseBody.count) byte long response body")
20+
/// }
21+
/// }
22+
/// ```
23+
/// **Note**: The class inheritance to `HTTPURLResponse` is to provide above mechanism, and actual
24+
/// implementation acts as a facade that stores an instance of `HTTPURLResponse` that delegates overidden methods to
25+
/// this stored property.
26+
public class AWSHTTPURLResponse: HTTPURLResponse {
27+
28+
/// The body of the response, if available
29+
public let body: Data?
30+
31+
private let response: HTTPURLResponse
32+
33+
init?(response: HTTPURLResponse, body: Data?) {
34+
self.body = body
35+
self.response = response
36+
37+
// Call the super class initializer with dummy values to satisfy the requirement of the inheritance.
38+
// Subsequent access to any properties of this instance (including `url`) will be delegated to
39+
// the `response`.
40+
super.init(url: URL(string: "dummyURL")!,
41+
statusCode: 0,
42+
httpVersion: nil,
43+
headerFields: nil)
44+
}
45+
46+
required init?(coder: NSCoder) {
47+
self.body = coder.decodeObject(forKey: "body") as? Data
48+
self.response = coder.decodeObject(forKey: "response") as? HTTPURLResponse ?? HTTPURLResponse()
49+
super.init(coder: coder)
50+
}
51+
52+
public override func encode(with coder: NSCoder) {
53+
coder.encode(body, forKey: "body")
54+
coder.encode(response, forKey: "response")
55+
super.encode(with: coder)
56+
}
57+
58+
public override var url: URL? {
59+
response.url
60+
}
61+
62+
public override var mimeType: String? {
63+
response.mimeType
64+
}
65+
66+
public override var expectedContentLength: Int64 {
67+
response.expectedContentLength
68+
}
69+
70+
public override var textEncodingName: String? {
71+
response.textEncodingName
72+
}
73+
74+
public override var suggestedFilename: String? {
75+
response.suggestedFilename
76+
}
77+
78+
public override var statusCode: Int {
79+
response.statusCode
80+
}
81+
82+
public override var allHeaderFields: [AnyHashable: Any] {
83+
response.allHeaderFields
84+
}
85+
86+
@available(iOS 13.0, *)
87+
public override func value(forHTTPHeaderField field: String) -> String? {
88+
response.value(forHTTPHeaderField: field)
89+
}
90+
}

AmplifyPlugins/API/AWSAPICategoryPluginIntegrationTests/REST/RESTWithIAMIntegrationTests/RESTWithIAMIntegrationTests.swift

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,21 @@
66
//
77

88
import XCTest
9-
@testable import Amplify
109
import AWSAPICategoryPlugin
11-
import AWSMobileClient
10+
import AmplifyPlugins
11+
12+
@testable import Amplify
1213
@testable import AmplifyTestCommon
1314

1415
class RESTWithIAMIntegrationTests: XCTestCase {
1516

1617
static let amplifyConfiguration = "RESTWithIAMIntegrationTests-amplifyconfiguration"
17-
static let awsconfiguration = "RESTWithIAMIntegrationTests-awsconfiguration"
1818

1919
override func setUp() {
2020

2121
do {
22-
let awsConfiguration = try TestConfigHelper.retrieveAWSConfiguration(
23-
forResource: RESTWithIAMIntegrationTests.awsconfiguration)
24-
AWSInfo.configureDefaultAWSInfo(awsConfiguration)
25-
26-
AuthHelper.initializeMobileClient()
27-
28-
Amplify.reset()
29-
3022
try Amplify.add(plugin: AWSAPIPlugin())
31-
23+
try Amplify.add(plugin: AWSCognitoAuthPlugin())
3224
let amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration(
3325
forResource: RESTWithIAMIntegrationTests.amplifyConfiguration)
3426
try Amplify.configure(amplifyConfig)
@@ -66,11 +58,21 @@ class RESTWithIAMIntegrationTests: XCTestCase {
6658
case .success(let data):
6759
XCTFail("Unexpected .complted event: \(data)")
6860
case .failure(let error):
69-
guard case let .httpStatusError(statusCode, _) = error else {
61+
guard case let .httpStatusError(statusCode, response) = error else {
7062
XCTFail("Error should be httpStatusError")
7163
return
7264
}
73-
65+
XCTAssertNotNil(response.url)
66+
XCTAssertEqual(response.mimeType, "application/json")
67+
XCTAssertEqual(response.expectedContentLength, 258)
68+
XCTAssertEqual(response.statusCode, 403)
69+
XCTAssertNotNil(response.allHeaderFields)
70+
if let awsResponse = response as? AWSHTTPURLResponse, let data = awsResponse.data {
71+
let dataString = String(decoding: data, as: UTF8.self)
72+
XCTAssertTrue(dataString.contains("not authorized"))
73+
} else {
74+
XCTFail("Missing response body")
75+
}
7476
XCTAssertEqual(statusCode, 403)
7577
failedInvoked.fulfill()
7678
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 XCTest
9+
@testable import AWSAPICategoryPlugin
10+
11+
class AWSHTTPURLResponseTests: XCTestCase {
12+
13+
func testAWSHTTPURLResponse() throws {
14+
let body = "responseBody".data(using: .utf8)
15+
let httpResponse = HTTPURLResponse(url: URL(string: "dummyString")!,
16+
statusCode: 200,
17+
httpVersion: "1.1",
18+
headerFields: ["key1": "value1",
19+
"key2": "value2"])!
20+
if let response = AWSHTTPURLResponse(response: httpResponse, body: body) {
21+
XCTAssertNotNil(response.body)
22+
XCTAssertNotNil(response.url)
23+
XCTAssertNil(response.mimeType)
24+
XCTAssertEqual(response.expectedContentLength, -1)
25+
XCTAssertNil(response.textEncodingName)
26+
XCTAssertNotNil(response.suggestedFilename)
27+
XCTAssertEqual(response.statusCode, 200)
28+
XCTAssertEqual(response.allHeaderFields.count, 2)
29+
30+
if #available(iOS 13.0, *) {
31+
XCTAssertNotNil(response.value(forHTTPHeaderField: "key1"))
32+
XCTAssertNotNil(response.value(forHTTPHeaderField: "key2"))
33+
}
34+
} else {
35+
XCTFail("Failed to initialize `AWSHTTPURLResponse`")
36+
}
37+
}
38+
39+
func testAWSHTTPURLResponseNSCoding() {
40+
let body = "responseBody".data(using: .utf8)
41+
let httpResponse = HTTPURLResponse(url: URL(string: "dummyString")!,
42+
statusCode: 200,
43+
httpVersion: "1.1",
44+
headerFields: ["key1": "value1",
45+
"key2": "value2"])!
46+
guard let response = AWSHTTPURLResponse(response: httpResponse, body: body) else {
47+
XCTFail("Failed to initialize `AWSHTTPURLResponse`")
48+
return
49+
}
50+
let data = NSKeyedArchiver.archivedData(withRootObject: response)
51+
XCTAssertNotNil(data)
52+
guard let unarchivedResponse = NSKeyedUnarchiver.unarchiveObject(with: data) as? AWSHTTPURLResponse else {
53+
XCTFail("Failed to unarchive `AWSHTTPURLResponse` data")
54+
return
55+
}
56+
XCTAssertNotNil(unarchivedResponse)
57+
XCTAssertNotNil(unarchivedResponse.body)
58+
XCTAssertNotNil(unarchivedResponse.url)
59+
XCTAssertNil(unarchivedResponse.mimeType)
60+
XCTAssertEqual(unarchivedResponse.expectedContentLength, -1)
61+
XCTAssertNil(unarchivedResponse.textEncodingName)
62+
XCTAssertNotNil(unarchivedResponse.suggestedFilename)
63+
XCTAssertEqual(unarchivedResponse.statusCode, 200)
64+
XCTAssertEqual(unarchivedResponse.allHeaderFields.count, 2)
65+
}
66+
}

AmplifyPlugins/API/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,4 @@ SPEC CHECKSUMS:
108108

109109
PODFILE CHECKSUM: 857e4ea8246683593b8a7db77014aeb7340c15a5
110110

111-
COCOAPODS: 1.9.3
111+
COCOAPODS: 1.10.1

0 commit comments

Comments
 (0)