Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replaced generic struct based AWSAppSyncClientError by a typed enum… #35

Merged
merged 5 commits into from
Nov 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 76 additions & 11 deletions AWSAppSyncClient/AWSAppSyncClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -524,20 +524,85 @@ public struct AWSAppSyncClientInfoError: Error, LocalizedError {
}
}

public struct AWSAppSyncClientError: Error, LocalizedError {

/// The body of the response.
public let body: Data?
/// Information about the response as provided by the server.
public let response: HTTPURLResponse?
let isInternalError: Bool
let additionalInfo: String?
public enum AWSAppSyncClientError: Error, LocalizedError {
case requestFailed(Data?, HTTPURLResponse?, Error?)
case noData(HTTPURLResponse)
case parseError(Data, HTTPURLResponse, Error?)
case authenticationError(Error)

public var errorDescription: String? {
if (isInternalError) {
return additionalInfo
let underlyingError: Error?
var message: String
let errorResponse: HTTPURLResponse?
switch self {
case .requestFailed(_, let response, let error):
errorResponse = response
underlyingError = error
message = "Did not receive a successful HTTP code."
case .noData(let response):
errorResponse = response
underlyingError = nil
message = "No Data received in response."
case .parseError(_, let response, let error):
underlyingError = error
errorResponse = response
message = "Could not parse response data."
case .authenticationError(let error):
underlyingError = error
errorResponse = nil
message = "Failed to authenticate request."
}

if let error = underlyingError {
message += " Error: \(error)"
}

if let unwrappedResponse = errorResponse {
return "(\(unwrappedResponse.statusCode) \(unwrappedResponse.statusCodeDescription)) \(message)"
} else {
return "\(message)"
}
}

@available(*, deprecated, message: "use the enum pattern matching instead")
public var body: Data? {
switch self {
case .parseError(let data, _, _):
return data
case .requestFailed(let data, _, _):
return data
case .noData, .authenticationError(_):
return nil
}
}

@available(*, deprecated, message: "use the enum pattern matching instead")
public var response: HTTPURLResponse? {
switch self {
case .parseError(_, let response, _):
return response
case .requestFailed(_, let response, _):
return response
case .noData, .authenticationError(_):
return nil
}
}

@available(*, deprecated)
var isInternalError: Bool {
return false
}

@available(*, deprecated, message: "use errorDescription instead")
var additionalInfo: String? {
switch self {
case .parseError(_, _, _):
return "Could not parse response data."
case .requestFailed(_, _, _):
return "Did not receive a successful HTTP code."
case .noData, .authenticationError(_):
return "No Data received in response."
}
return "(\(response!.statusCode) \(response!.statusCodeDescription)) \(additionalInfo ?? "")"
}
}

Expand Down
84 changes: 45 additions & 39 deletions AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,19 +174,17 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {
internal func sendGraphQLRequest(mutableRequest: NSMutableURLRequest,
retryHandler: AWSAppSyncRetryHandler,
networkTransportOperation: AWSAppSyncHTTPNetworkTransportOperation,
completionHandler: @escaping (JSONObject?, Error?) -> Void) {
updateRequestWithAuthInformation(mutableRequest: mutableRequest, completionHandler: { error in
guard error == nil else {
completionHandler(nil, error)
return
}
let dataTask = self.sendNetworkRequest(request: mutableRequest as URLRequest, completionHandler: {[weak self] (jsonBody, httpResponse, error) in
guard error == nil else {
if let appsyncError = error as? AWSAppSyncClientError,
let response = appsyncError.response,
let body = appsyncError.body {
let (shouldRetry, backoffTime) = retryHandler.shouldRetryRequest(httpResponse: response, body: body)
if (shouldRetry == true && backoffTime != nil) {
completionHandler: @escaping (JSONObject?, AWSAppSyncClientError?) -> Void) {
updateRequestWithAuthInformation(mutableRequest: mutableRequest, completionHandler: { result in
switch result {
case .success:
let dataTask = self.sendNetworkRequest(request: mutableRequest as URLRequest, completionHandler: {[weak self] (result) in
switch result {
case .success(let jsonBody):
completionHandler(jsonBody, nil)
case .failure(let error):
let (shouldRetry, backoffTime) = retryHandler.shouldRetryRequest(for: error)
if (shouldRetry && backoffTime != nil) {
let _ = self?.executeAfter(milliseconds: backoffTime!, queue: DispatchQueue.global(qos: .userInitiated), block: {
self?.sendGraphQLRequest(mutableRequest: mutableRequest,
retryHandler: retryHandler,
Expand All @@ -196,11 +194,12 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {
completionHandler(nil, error)
}
}
return
}
completionHandler(jsonBody, nil)
})
networkTransportOperation.dataTask = dataTask
})
networkTransportOperation.dataTask = dataTask
case .failure(let error):
completionHandler(nil, AWSAppSyncClientError.authenticationError(error))
}

})
}

Expand All @@ -210,12 +209,12 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {
/// - request: The URL request to be sent
/// - completionHandler: The completion handler which will be called once the request is completed
/// - Returns: URLSessionDataTask cancellable object
internal func sendNetworkRequest(request: URLRequest, completionHandler: @escaping ((JSONObject?, HTTPURLResponse?, Error?) -> Void)) -> URLSessionDataTask {
internal func sendNetworkRequest(request: URLRequest, completionHandler: @escaping (Result<JSONObject, AWSAppSyncClientError>) -> Void) -> URLSessionDataTask {

let dataTask = self.session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error:Error?) in

if error != nil {
completionHandler(nil, nil, error)
if let error = error {
completionHandler(.failure(AWSAppSyncClientError.requestFailed(data, nil, error)))
return
}

Expand All @@ -224,24 +223,25 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {
}

if (!httpResponse.isSuccessful) {
let err = AWSAppSyncClientError(body: data, response: httpResponse, isInternalError: false, additionalInfo: "Did not receive a successful HTTP code.")
completionHandler(nil, nil, err)
completionHandler(.failure(AWSAppSyncClientError.requestFailed(data, httpResponse, error)))

return
}

guard let data = data else {
let err = AWSAppSyncClientError(body: nil, response: httpResponse, isInternalError: false, additionalInfo: "No Data received in response.")
completionHandler(nil, nil, err)
completionHandler(.failure(AWSAppSyncClientError.noData(httpResponse)))

return
}
do {
guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else {
throw AWSAppSyncClientError(body: data, response: httpResponse, isInternalError: false, additionalInfo: "Could not parse response data.")
completionHandler(.failure(AWSAppSyncClientError.parseError(data, httpResponse, nil)))

return
}

completionHandler(body, httpResponse, error)
completionHandler(.success(body))
} catch {
completionHandler(nil, nil, error)
completionHandler(.failure(AWSAppSyncClientError.parseError(data, httpResponse, error)))
}

})
Expand All @@ -253,7 +253,7 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {
/// Updates the sendRequest with the appropriate authentication parameters
/// In the case of a token retrieval error, the errorCallback is invoked
private func updateRequestWithAuthInformation(mutableRequest: NSMutableURLRequest,
completionHandler: @escaping ((Error?) -> Void)) -> Void {
completionHandler: @escaping (Result<Void, Error>) -> Void) -> Void {

switch self.authType {

Expand All @@ -262,33 +262,33 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {

signer.interceptRequest(mutableRequest).continueWith { task in
if let error = task.error {
completionHandler(error)
completionHandler(.failure(error))
} else {
completionHandler(nil)
completionHandler(.success(()))
}
return nil
}
case .apiKey:
mutableRequest.setValue(self.apiKeyAuthProvider!.getAPIKey(), forHTTPHeaderField: "x-api-key")
completionHandler(nil)
completionHandler(.success(()))
case .oidcToken:
if let provider = self.oidcAuthProvider as? AWSOIDCAuthProviderAsync {

provider.getLatestAuthToken { (token, error) in
if let error = error {
completionHandler(error)
completionHandler(.failure(error))
}
else if let token = token {
mutableRequest.setValue(token, forHTTPHeaderField: "authorization")
completionHandler(nil)
completionHandler(.success(()))
}
else {
fatalError("Invalid data returned in token callback")
}
}
} else if let provider = self.oidcAuthProvider {
mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization")
completionHandler(nil)
completionHandler(.success(()))
} else {
fatalError("Authentication provider not set")
}
Expand All @@ -297,19 +297,19 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {

provider.getLatestAuthToken { (token, error) in
if let error = error {
completionHandler(error)
completionHandler(.failure(error))
}
else if let token = token {
mutableRequest.setValue(token, forHTTPHeaderField: "authorization")
completionHandler(nil)
completionHandler(.success(()))
}
else {
fatalError("Invalid data returned in token callback")
}
}
} else if let provider = self.userPoolsAuthProvider {
mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization")
completionHandler(nil)
completionHandler(.success(()))
} else {
fatalError("Authentication provider not set")
}
Expand Down Expand Up @@ -434,4 +434,10 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport {
self.dataTask?.cancel()
}
}

internal enum Result<V, E> {
case success(V)
case failure(E)
}

}
24 changes: 20 additions & 4 deletions AWSAppSyncClient/AWSAppSyncRetryHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,33 @@ internal class AWSAppSyncRetryHandler {

/// Returns if a request should be retried
///
/// - Parameter errorResponse: The error response returned by the service.
/// - Parameter error: The error returned by the service.
/// - Returns: If the request should be retried and if yes, after how much time.
func shouldRetryRequest(httpResponse: HTTPURLResponse, body: Data) -> (Bool, Int?) {
func shouldRetryRequest(for error: AWSAppSyncClientError) -> (Bool, Int?) {
let httpResponse: HTTPURLResponse?
switch error {
case .requestFailed(_, let reponse, _):
httpResponse = reponse
case .noData(let response):
httpResponse = response
case .parseError(_, let response, _):
httpResponse = response
case .authenticationError(_):
httpResponse = nil
}

guard let unwrappedResponse = httpResponse else {
return (false, nil)
}

currentAttemptNumber += 1
let waitTime = httpResponse.allHeaderFields["Retry-After"] as? Int
let waitTime = unwrappedResponse.allHeaderFields["Retry-After"] as? Int
if let waitTime = waitTime {
let waitMillis = waitTime * 1000
return (true, waitMillis)
}

switch httpResponse.statusCode {
switch unwrappedResponse.statusCode {
case 500 ... 599, 429:
let waitMillis = Int(Double(pow(2.0, currentAttemptNumber) as NSNumber) * 100.0 + Double(getRandomBetween0And1() * AWSAppSyncRetryHandler.JITTER))
if (waitMillis > AWSAppSyncRetryHandler.MAX_RETRY_WAIT_MILLIS) {
Expand Down
6 changes: 5 additions & 1 deletion AWSAppSyncTests/AuthProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,11 @@ class AuthProviderTests: XCTestCase {

client.perform(mutation: mutation, resultHandler: { (_, error) in
XCTAssert(error != nil)
XCTAssert(error is AuthProviderTestError)
if case AWSAppSyncClientError.authenticationError(let authError) = error as! AWSAppSyncClientError {
XCTAssert(authError is AuthProviderTestError)
} else {
XCTAssertTrue(false, "Error of unexpected type")
}
expectation2.fulfill()
})

Expand Down