From 36d582cbfd48baf31905a26402bf6d2a5b48d8ed Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Fri, 30 Oct 2020 11:57:34 +0000 Subject: [PATCH] Add additional fields to AWSErrorContext (#392) * Added decoding of additional error fields * Parse errors for additional information * Swift format --- Sources/SotoCore/Errors/Error.swift | 9 ++- Sources/SotoCore/Message/AWSResponse.swift | 89 ++++++++++++++++------ Tests/SotoCoreTests/AWSResponseTests.swift | 32 ++++++-- 3 files changed, 99 insertions(+), 31 deletions(-) diff --git a/Sources/SotoCore/Errors/Error.swift b/Sources/SotoCore/Errors/Error.swift index b026a775c..9fbb3d017 100644 --- a/Sources/SotoCore/Errors/Error.swift +++ b/Sources/SotoCore/Errors/Error.swift @@ -39,11 +39,18 @@ public struct AWSErrorContext { public let message: String public let responseCode: HTTPResponseStatus public let headers: HTTPHeaders + public let additionalFields: [String: String] - internal init(message: String, responseCode: HTTPResponseStatus, headers: HTTPHeaders = [:]) { + internal init( + message: String, + responseCode: HTTPResponseStatus, + headers: HTTPHeaders = [:], + additionalFields: [String: String] = [:] + ) { self.message = message self.responseCode = responseCode self.headers = headers + self.additionalFields = additionalFields } } diff --git a/Sources/SotoCore/Message/AWSResponse.swift b/Sources/SotoCore/Message/AWSResponse.swift index 84bf23372..d201b108e 100644 --- a/Sources/SotoCore/Message/AWSResponse.swift +++ b/Sources/SotoCore/Message/AWSResponse.swift @@ -239,7 +239,8 @@ public struct AWSResponse { let context = AWSErrorContext( message: errorMessage.message, responseCode: self.status, - headers: .init(headers.map { ($0.key, String(describing: $0.value)) }) + headers: .init(headers.map { ($0.key, String(describing: $0.value)) }), + additionalFields: errorMessage.additionalFields ) if let errorType = serviceConfig.errorType { @@ -261,52 +262,92 @@ public struct AWSResponse { return nil } + /// Error used by XML output private struct XMLQueryError: Codable, APIError { var code: String? - var message: String + let message: String + let additionalFields: [String: String] - private enum CodingKeys: String, CodingKey { - case code = "Code" - case message = "Message" + init(from decoder: Decoder) throws { + // use `ErrorCodingKey` so we get extract additional keys from `container.allKeys` + let container = try decoder.container(keyedBy: ErrorCodingKey.self) + self.code = try container.decodeIfPresent(String.self, forKey: .init("Code")) + self.message = try container.decode(String.self, forKey: .init("Message")) + + var additionalFields: [String: String] = [:] + for key in container.allKeys { + guard key.stringValue != "Code", key.stringValue != "Message" else { continue } + additionalFields[key.stringValue] = try container.decodeIfPresent(String.self, forKey: key) + } + self.additionalFields = additionalFields } } + /// Error used by JSON output private struct JSONError: Decodable, APIError { var code: String? - var message: String + let message: String + let additionalFields: [String: String] init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.code = try container.decode(String?.self, forKey: .code) - self.message = try container.decodeIfPresent(String.self, forKey: .message) ?? container.decode(String.self, forKey: .Message) - } - - private enum CodingKeys: String, CodingKey { - case code = "__type" - case message - case Message + // use `ErrorCodingKey` so we get extract additional keys from `container.allKeys` + let container = try decoder.container(keyedBy: ErrorCodingKey.self) + self.code = try container.decodeIfPresent(String.self, forKey: .init("__type")) + self.message = try container.decodeIfPresent(String.self, forKey: .init("message")) ?? container.decode(String.self, forKey: .init("Message")) + + var additionalFields: [String: String] = [:] + for key in container.allKeys { + guard key.stringValue != "__type", key.stringValue != "message", key.stringValue != "Message" else { continue } + additionalFields[key.stringValue] = try container.decodeIfPresent(String.self, forKey: key) + } + self.additionalFields = additionalFields } } + /// Error used by REST JSON output private struct RESTJSONError: Decodable, APIError { var code: String? - var message: String + let message: String + let additionalFields: [String: String] init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.code = try container.decodeIfPresent(String.self, forKey: .code) - self.message = try container.decodeIfPresent(String.self, forKey: .message) ?? container.decode(String.self, forKey: .Message) + // use `ErrorCodingKey` so we get extract additional keys from `container.allKeys` + let container = try decoder.container(keyedBy: ErrorCodingKey.self) + self.code = try container.decodeIfPresent(String.self, forKey: .init("code")) + self.message = try container.decodeIfPresent(String.self, forKey: .init("message")) ?? container.decode(String.self, forKey: .init("Message")) + + var additionalFields: [String: String] = [:] + for key in container.allKeys { + guard key.stringValue != "code", key.stringValue != "message", key.stringValue != "Message" else { continue } + additionalFields[key.stringValue] = try container.decodeIfPresent(String.self, forKey: key) + } + self.additionalFields = additionalFields + } + } + + /// CodingKey used when decoding Errors + private struct ErrorCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + + init(_ stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil } - private enum CodingKeys: String, CodingKey { - case code - case message - case Message + init?(intValue: Int) { + return nil } } } private protocol APIError { var code: String? { get set } - var message: String { get set } + var message: String { get } + var additionalFields: [String: String] { get } } diff --git a/Tests/SotoCoreTests/AWSResponseTests.swift b/Tests/SotoCoreTests/AWSResponseTests.swift index 55dea6a1a..0b8c95a3e 100644 --- a/Tests/SotoCoreTests/AWSResponseTests.swift +++ b/Tests/SotoCoreTests/AWSResponseTests.swift @@ -224,7 +224,7 @@ class AWSResponseTests: XCTestCase { let response = AWSHTTPResponseImpl( status: .notFound, headers: HTTPHeaders(), - bodyData: "{\"__type\":\"ResourceNotFoundException\", \"Message\": \"Donald Where's Your Troosers?\"}".data(using: .utf8)! + bodyData: #"{"__type":"ResourceNotFoundException", "Message": "Donald Where's Your Troosers?", "fault": "client"}"# .data(using: .utf8)! ) let service = createServiceConfig(serviceProtocol: .json(version: "1.1"), errorType: ServiceErrorType.self) @@ -234,13 +234,14 @@ class AWSResponseTests: XCTestCase { XCTAssertEqual(error, ServiceErrorType.resourceNotFoundException) XCTAssertEqual(error?.message, "Donald Where's Your Troosers?") XCTAssertEqual(error?.context?.responseCode, .notFound) + XCTAssertEqual(error?.context?.additionalFields["fault"], "client") } func testRestJSONError() { let response = AWSHTTPResponseImpl( status: .notFound, headers: ["x-amzn-errortype": "ResourceNotFoundException"], - bodyData: Data("{\"message\": \"Donald Where's Your Troosers?\"}".utf8) + bodyData: Data(#"{"message": "Donald Where's Your Troosers?", "Fault": "Client"}"# .utf8) ) let service = createServiceConfig(serviceProtocol: .restjson, errorType: ServiceErrorType.self) @@ -250,13 +251,15 @@ class AWSResponseTests: XCTestCase { XCTAssertEqual(error, ServiceErrorType.resourceNotFoundException) XCTAssertEqual(error?.message, "Donald Where's Your Troosers?") XCTAssertEqual(error?.context?.responseCode, .notFound) + XCTAssertEqual(error?.context?.additionalFields["Fault"], "Client") } func testRestJSONErrorV2() { + // Capitalized "Message" let response = AWSHTTPResponseImpl( status: .notFound, headers: ["x-amzn-errortype": "ResourceNotFoundException"], - bodyData: Data("{\"Message\": \"Donald Where's Your Troosers?\"}".utf8) + bodyData: Data(#"{"Message": "Donald Where's Your Troosers?"}"# .utf8) ) let service = createServiceConfig(serviceProtocol: .restjson, errorType: ServiceErrorType.self) @@ -272,7 +275,7 @@ class AWSResponseTests: XCTestCase { let response = AWSHTTPResponseImpl( status: .notFound, headers: HTTPHeaders(), - bodyData: "NoSuchKeyIt doesn't exist".data(using: .utf8)! + bodyData: "NoSuchKeyIt doesn't existclient".data(using: .utf8)! ) let service = createServiceConfig(serviceProtocol: .restxml, errorType: ServiceErrorType.self) @@ -282,13 +285,14 @@ class AWSResponseTests: XCTestCase { XCTAssertEqual(error, ServiceErrorType.noSuchKey) XCTAssertEqual(error?.message, "It doesn't exist") XCTAssertEqual(error?.context?.responseCode, .notFound) + XCTAssertEqual(error?.context?.additionalFields["fault"], "client") } func testQueryError() { let response = AWSHTTPResponseImpl( status: .notFound, headers: HTTPHeaders(), - bodyData: "MessageRejectedDon't like it".data(using: .utf8)! + bodyData: "MessageRejectedDon't like itclient".data(using: .utf8)! ) let queryService = createServiceConfig(serviceProtocol: .query, errorType: ServiceErrorType.self) @@ -298,13 +302,14 @@ class AWSResponseTests: XCTestCase { XCTAssertEqual(error, ServiceErrorType.messageRejected) XCTAssertEqual(error?.message, "Don't like it") XCTAssertEqual(error?.context?.responseCode, .notFound) + XCTAssertEqual(error?.context?.additionalFields["fault"], "client") } func testEC2Error() { let response = AWSHTTPResponseImpl( status: .notFound, headers: HTTPHeaders(), - bodyData: "NoSuchKeyIt doesn't exist".data(using: .utf8)! + bodyData: "NoSuchKeyIt doesn't existclient".data(using: .utf8)! ) let service = createServiceConfig(serviceProtocol: .ec2) @@ -314,6 +319,21 @@ class AWSResponseTests: XCTestCase { XCTAssertEqual(error?.errorCode, "NoSuchKey") XCTAssertEqual(error?.message, "It doesn't exist") XCTAssertEqual(error?.context?.responseCode, .notFound) + XCTAssertEqual(error?.context?.additionalFields["fault"], "client") + } + + func testAdditionalErrorFields() { + let response = AWSHTTPResponseImpl( + status: .notFound, + headers: HTTPHeaders(), + bodyData: "NoSuchKeyIt doesn't existclient".data(using: .utf8)! + ) + let service = createServiceConfig(serviceProtocol: .restxml) + + var awsResponse: AWSResponse? + XCTAssertNoThrow(awsResponse = try AWSResponse(from: response, serviceProtocol: .ec2, raw: false)) + let error = awsResponse?.generateError(serviceConfig: service, logger: TestEnvironment.logger) as? AWSResponseError + XCTAssertEqual(error?.context?.additionalFields["fault"], "client") } // MARK: Miscellaneous tests