Skip to content

Commit

Permalink
Add additional fields to AWSErrorContext (#392)
Browse files Browse the repository at this point in the history
* Added decoding of additional error fields

* Parse errors for additional information

* Swift format
  • Loading branch information
adam-fowler authored Oct 30, 2020
1 parent 7ad4dfa commit 36d582c
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 31 deletions.
9 changes: 8 additions & 1 deletion Sources/SotoCore/Errors/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
89 changes: 65 additions & 24 deletions Sources/SotoCore/Message/AWSResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 }
}
32 changes: 26 additions & 6 deletions Tests/SotoCoreTests/AWSResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -272,7 +275,7 @@ class AWSResponseTests: XCTestCase {
let response = AWSHTTPResponseImpl(
status: .notFound,
headers: HTTPHeaders(),
bodyData: "<Error><Code>NoSuchKey</Code><Message>It doesn't exist</Message></Error>".data(using: .utf8)!
bodyData: "<Error><Code>NoSuchKey</Code><Message>It doesn't exist</Message><fault>client</fault></Error>".data(using: .utf8)!
)
let service = createServiceConfig(serviceProtocol: .restxml, errorType: ServiceErrorType.self)

Expand All @@ -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: "<ErrorResponse><Error><Code>MessageRejected</Code><Message>Don't like it</Message></Error></ErrorResponse>".data(using: .utf8)!
bodyData: "<ErrorResponse><Error><Code>MessageRejected</Code><Message>Don't like it</Message><fault>client</fault></Error></ErrorResponse>".data(using: .utf8)!
)
let queryService = createServiceConfig(serviceProtocol: .query, errorType: ServiceErrorType.self)

Expand All @@ -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: "<Errors><Error><Code>NoSuchKey</Code><Message>It doesn't exist</Message></Error></Errors>".data(using: .utf8)!
bodyData: "<Errors><Error><Code>NoSuchKey</Code><Message>It doesn't exist</Message><fault>client</fault></Error></Errors>".data(using: .utf8)!
)
let service = createServiceConfig(serviceProtocol: .ec2)

Expand All @@ -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: "<Errors><Error><Code>NoSuchKey</Code><Message>It doesn't exist</Message><fault>client</fault></Error></Errors>".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
Expand Down

0 comments on commit 36d582c

Please sign in to comment.