Skip to content

Commit

Permalink
fix: More detailed error handling messages (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
CameronMcWilliam authored and djones6 committed Oct 8, 2019
1 parent b15bbb1 commit 96cc0df
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 57 deletions.
74 changes: 39 additions & 35 deletions Sources/KituraKit/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,18 @@ public class KituraKit {
request.responseData { result in
switch result {
case .success(let response):
guard let item: O = try? self.decoder.decode(O.self, from: response.body),
let locationHeader = response.headers["Location"].first,
let id = try? Id.init(value: locationHeader)
else {
respondWith(nil, nil, RequestError.clientDeserializationError)
return
do {
let item: O = try self.decoder.decode(O.self, from: response.body)
guard let locationHeader = response.headers["Location"].first,
let id = try? Id.init(value: locationHeader)
else {
respondWith(nil, nil, RequestError(.clientDecodingError, body: "Missing location header."))
return
}
respondWith(id, item, nil)
} catch {
respondWith(nil, nil, RequestError.clientDecodingError(underlyingError: error))
}
respondWith(id, item, nil)
case .failure(let error):
Log.error("POST failure: \(error)")
respondWith(nil, nil, constructRequestError(from: error, data: error.responseData))
Expand Down Expand Up @@ -373,16 +377,17 @@ public class KituraKit {
/// - Parameter route: The custom route KituraKit points to during REST requests.
/// - Parameter queryParams: The QueryParam structure containing the route's query parameters
public func get<O: Codable, Q: QueryParams>(_ route: String, query: Q, credentials: ClientCredentials? = nil, respondWith: @escaping CodableArrayResultClosure<O>) {
let credentials = (credentials ?? defaultCredentials)
guard let queryItems: [URLQueryItem] = try? QueryEncoder().encode(query) else {
respondWith(nil, .clientSerializationError)
return
do {
let credentials = (credentials ?? defaultCredentials)
let queryItems: [URLQueryItem] = try QueryEncoder().encode(query)
let request = RestRequest(method: .get, url: baseURL.appendingPathComponent(route).absoluteString, insecure: self.containsSelfSignedCert, clientCertificate: self.clientCertificate)
request.headerParameters = credentials?.getHeaders() ?? [:]
request.acceptType = mediaType
request.contentType = mediaType
request.handle(decoder: decoder, respondWith, queryItems: queryItems)
} catch {
respondWith(nil, RequestError.clientEncodingError(underlyingError: error))
}
let request = RestRequest(method: .get, url: baseURL.appendingPathComponent(route).absoluteString, insecure: self.containsSelfSignedCert, clientCertificate: self.clientCertificate)
request.headerParameters = credentials?.getHeaders() ?? [:]
request.acceptType = mediaType
request.contentType = mediaType
request.handle(decoder: decoder, respondWith, queryItems: queryItems)
}

/// Deletes data at a designated route using a the specified Query Parameters.
Expand All @@ -404,16 +409,17 @@ public class KituraKit {
/// - Parameter route: The custom route KituraKit points to during REST requests.
/// - Parameter queryParams: The QueryParam structure containing the route's query parameters
public func delete<Q: QueryParams>(_ route: String, query: Q, credentials: ClientCredentials? = nil, respondWith: @escaping ResultClosure) {
let credentials = (credentials ?? defaultCredentials)
guard let queryItems: [URLQueryItem] = try? QueryEncoder().encode(query) else {
respondWith(.clientSerializationError)
return
do {
let credentials = (credentials ?? defaultCredentials)
let queryItems: [URLQueryItem] = try QueryEncoder().encode(query)
let request = RestRequest(method: .delete, url: baseURL.appendingPathComponent(route).absoluteString, insecure: self.containsSelfSignedCert, clientCertificate: self.clientCertificate)
request.headerParameters = credentials?.getHeaders() ?? [:]
request.acceptType = mediaType
request.contentType = mediaType
request.handleDelete(respondWith, queryItems: queryItems)
} catch {
respondWith(RequestError.clientEncodingError(underlyingError: error))
}
let request = RestRequest(method: .delete, url: baseURL.appendingPathComponent(route).absoluteString, insecure: self.containsSelfSignedCert, clientCertificate: self.clientCertificate)
request.headerParameters = credentials?.getHeaders() ?? [:]
request.acceptType = mediaType
request.contentType = mediaType
request.handleDelete(respondWith, queryItems: queryItems)
}
}

Expand Down Expand Up @@ -447,26 +453,24 @@ extension RestRequest {

/// Default success response handler for CodableArrayResultClosures and CodableResultClosures
private func defaultCodableHandler<O: Codable>(decoder: BodyDecoder, _ data: Data, respondWith: (O?, RequestError?) -> ()) {
guard let items: O = try? decoder.decode(O.self, from: data) else {
respondWith(nil, .clientDeserializationError)
return
do {
let items: O = try decoder.decode(O.self, from: data)
respondWith(items, nil)
} catch {
respondWith(nil, RequestError.clientDecodingError(underlyingError: error))
}
respondWith(items, nil)
}

/// Default failure response handler for CodableArrayResultClosures and CodableResultClosures
private func defaultErrorHandler<O: Codable>(_ error: Error, data: Data?, respondWith: (O?, RequestError?) -> ()) {
private func defaultErrorHandler<O: Codable>(_ error: RestError, data: Data?, respondWith: (O?, RequestError?) -> ()) {
respondWith(nil, constructRequestError(from: error, data: data))
}
}

// Convert an Error to a RequestError, mapping HTTP error codes over if given a
// SwiftyRequest.RestError. Decorate the RequestError with Data if provided
fileprivate func constructRequestError(from error: Error, data: Data?) -> RequestError {
var requestError = RequestError.clientConnectionError
if let restError = error as? RestError {
requestError = RequestError(restError: restError)
}
fileprivate func constructRequestError(from restError: RestError, data: Data?) -> RequestError {
var requestError = RequestError(restError: restError)
if let data = data {
do {
// TODO: Check Content-Type for format, assuming JSON for now
Expand Down
60 changes: 40 additions & 20 deletions Sources/KituraKit/RequestErrorExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,55 +23,75 @@ extension RequestError {

/// An initializer to set up the client error codes.
/// - Parameter clientErrorCode: The custom error code for the client.
public init(clientErrorCode: Int, clientErrorDescription: String) {
self.init(rawValue: clientErrorCode, reason: clientErrorDescription)
public init(clientErrorCode: Int, clientErrorDescription: String, underlyingError: Error? = nil) {
if let error = underlyingError {
self.init(rawValue: clientErrorCode, reason: clientErrorDescription + " - underlying error: \(error)")
} else {
self.init(rawValue: clientErrorCode, reason: clientErrorDescription)
}
}

/// An HTTP 600 unknown error
public static var clientErrorUnknown = RequestError(clientErrorCode: 600, clientErrorDescription: "An unknown error occurred")
public static let clientErrorUnknown = RequestError(clientErrorCode: 600, clientErrorDescription: "An unknown error occurred")

/// An HTTP 601 connection error
public static var clientConnectionError = RequestError(clientErrorCode: 601, clientErrorDescription: "A connection error occurred, cannot connect to the server. Please ensure that the server is started and running, with the correct port and URL ('ToDoServer' if using the sample app).")
public static let clientConnectionError = RequestError(clientErrorCode: 601, clientErrorDescription: "A connection error occurred, cannot connect to the server. Please ensure that the server is started and running, with the correct port and URL ('ToDoServer' if using the sample app).")

/// An HTTP 602 no data error
public static var clientNoData = RequestError(clientErrorCode: 602, clientErrorDescription: "A no data error occurred. Please ensure that data exists, and is being passed to the correct destination.")
public static let clientNoData = RequestError(clientErrorCode: 602, clientErrorDescription: "A no data error occurred.")

/// An HTTP 603 serialization error
public static var clientSerializationError = RequestError(clientErrorCode: 603, clientErrorDescription: "A serialization error occurred. Please ensure that the type of the data being serialized is correct.")
public static let clientSerializationError = RequestError.clientSerializationError(underlyingError: nil)

/// An HTTP 604 deserialization error
public static var clientDeserializationError = RequestError(clientErrorCode: 604, clientErrorDescription: "A deserialization error occurred. Please ensure that the type of the data being deserialized is correct.")
public static let clientDeserializationError = RequestError.clientDecodingError(underlyingError: nil)

/// An HTTP 605 encoding error
public static var clientEncodingError = RequestError(clientErrorCode: 605, clientErrorDescription: "An encoding error occurred. Please ensure that the types and format of the data being passed is correct.")
public static let clientEncodingError = RequestError.clientEncodingError(underlyingError: nil)

/// An HTTP 606 file manager error
public static var clientFileManagerError = RequestError(clientErrorCode: 606, clientErrorDescription: "A file manager error occurred. Please ensure that the file exists, and correct permissions to the file manager are present for the user.")
public static let clientFileManagerError = RequestError(clientErrorCode: 606, clientErrorDescription: "A file manager error occurred. Please ensure that the file exists, and correct permissions to the file manager are present for the user.")

/// An HTTP 607 invalid file error
public static var clientInvalidFile = RequestError(clientErrorCode: 607, clientErrorDescription: "An invalid file error occurred. Please ensure that the file type and contents are correct.")
public static let clientInvalidFile = RequestError(clientErrorCode: 607, clientErrorDescription: "An invalid file error occurred.")

/// An HTTP 608 invalid substitution error
public static var clientInvalidSubstitution = RequestError(clientErrorCode: 608, clientErrorDescription: "An invalid substitution error occurred. Please ensure that the data being substituted is correct.")
public static let clientInvalidSubstitution = RequestError(clientErrorCode: 608, clientErrorDescription: "An invalid substitution error occurred.")

/// An HTTP 609 encoding error
public static var clientDecodingError = RequestError(clientErrorCode: 609, clientErrorDescription: "A decoding error occurred. Please ensure that the types and format of the data being received is correct.")
public static let clientDecodingError = RequestError.clientDecodingError(underlyingError: nil)

static func clientDecodingError(underlyingError: Error?) -> RequestError {
return RequestError(clientErrorCode: 609, clientErrorDescription: "A decoding error occurred.", underlyingError: underlyingError)
}

static func clientEncodingError(underlyingError: Error?) -> RequestError {
return RequestError(clientErrorCode: 605, clientErrorDescription: "An encoding error occurred.", underlyingError: underlyingError)
}

static func clientSerializationError(underlyingError: Error?) -> RequestError {
return RequestError(clientErrorCode: 603, clientErrorDescription: "A serialization error occurred.", underlyingError: underlyingError)
}
}

/// An extension to Kitura RequestErrors with additional error codes specifically for the client.
extension RequestError {


static func makeRequestError(_ base: RequestError, underlyingError: RestError) -> RequestError {
return RequestError(clientErrorCode: base.rawValue, clientErrorDescription: base.reason, underlyingError: underlyingError)
}

/// An initializer to switch between different error types.
/// - Parameter restError: The custom error type for the client.
public init(restError: RestError) {
switch restError {
case .noData: self = .clientNoData
case .serializationError: self = .clientSerializationError
case .encodingError: self = .clientEncodingError
case .decodingError: self = .clientDecodingError
case .fileManagerError: self = .clientFileManagerError
case .invalidFile: self = .clientInvalidFile
case .invalidSubstitution: self = .clientInvalidSubstitution
case .noData: self = RequestError.makeRequestError(.clientNoData, underlyingError: restError)
case .serializationError: self = RequestError.makeRequestError(.clientSerializationError, underlyingError: restError)
case .encodingError: self = RequestError.makeRequestError(.clientEncodingError, underlyingError: restError)
case .decodingError: self = RequestError.makeRequestError(.clientDecodingError, underlyingError: restError)
case .fileManagerError: self = RequestError.makeRequestError(.clientFileManagerError, underlyingError: restError)
case .invalidFile: self = RequestError.makeRequestError(.clientInvalidFile, underlyingError: restError)
case .invalidSubstitution: self = RequestError.makeRequestError(.clientInvalidSubstitution, underlyingError: restError)
case .invalidURL: fallthrough // Will not occur: Client can only be initialized with a valid URL
case .downloadError: fallthrough // Will not occur: API is not used by KituraKit
case .errorStatusCode: fallthrough
Expand Down
8 changes: 8 additions & 0 deletions TestServer/Sources/TestServer/Controller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ public class Controller {
respondWith(user.id, user, nil)
}

router.post("/invaliduser") { (user: User?, respondWith: (Int?, User?, RequestError?) -> Void) in
guard let user = user else {
respondWith(nil, nil, .badRequest)
return
}
respondWith(1, user, nil)
}

router.put("/users") { (id: Int, user: User?, respondWith: (User?, RequestError?) -> Void) in
self.userStore[String(id)] = user
respondWith(user, nil)
Expand Down
2 changes: 0 additions & 2 deletions TestServer/Sources/TestServer/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,3 @@ struct JWTUser: Codable, Equatable {
return (lhs.name == rhs.name)
}
}


19 changes: 19 additions & 0 deletions Tests/KituraKitTests/MainTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,25 @@ class MainTests: XCTestCase {
waitForExpectations(timeout: 3.0, handler: nil)
}

// Deliberately uses a type that is mismatched with the server, in order to decode more fields from a
// response than the server has sent, leading to a decoding error.
func testClientPostErrorObject() {

let expectation1 = expectation(description: "An error is received from the server")
let invalidUser = AugmentedUser(id: 5, name: "John Doe", date: date, age: 20)
client.post("/invaliduser", data: invalidUser) { (id: Int?, returnedItem: AugmentedUser?, error: RequestError?) -> Void in
let errorString = String(describing: error)
// Can't access underlying error directly, so we check for evidence of the underlying error in the returned RequestError
if errorString.contains("keyNotFound(CodingKeys(stringValue: \"age\", intValue: nil)") {
expectation1.fulfill()
} else {
XCTFail("Failed to get expected error: \(String(describing: error))")
return
}
}
waitForExpectations(timeout: 3.0, handler: nil)
}

func testClientPut() {
let expectation1 = expectation(description: "A response is received from the server -> user")

Expand Down
17 changes: 17 additions & 0 deletions Tests/KituraKitTests/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,21 @@ struct JWTUser: Codable, Equatable {
}
}

public struct AugmentedUser: Codable, Equatable {
public let id: Int
public let name: String
public let date: Date
public let age: Int
public init(id: Int, name: String, date: Date, age: Int) {
self.id = id
self.name = name
self.date = date
self.age = age
}

public static func ==(lhs: AugmentedUser, rhs: AugmentedUser) -> Bool {
return (lhs.id == rhs.id) && (lhs.name == rhs.name) && (lhs.date == rhs.date) && (lhs.age == rhs.age)
}

}

0 comments on commit 96cc0df

Please sign in to comment.