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: "NoSuchKey
It doesn't exist".data(using: .utf8)!
+ bodyData: "NoSuchKey
It 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: "MessageRejected
Don't like it".data(using: .utf8)!
+ bodyData: "MessageRejected
Don'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: "NoSuchKey
It doesn't exist".data(using: .utf8)!
+ bodyData: "NoSuchKey
It 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: "NoSuchKey
It 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