From 35a85847a46182aa553ff5988d76de6b1b1ccd6a Mon Sep 17 00:00:00 2001 From: samgilmore <30483214+samgilmore@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:40:15 -0400 Subject: [PATCH] Add retry and timeout logic --- Sources/SwiftNetKit/BaseRequest.swift | 34 ----- Sources/SwiftNetKit/Models/NetworkError.swift | 1 + Sources/SwiftNetKit/NetworkService.swift | 141 +++++++++++------- .../Protocols/NetworkServiceProtocol.swift | 14 +- .../Protocols/RequestProtocol.swift | 36 +++++ 5 files changed, 139 insertions(+), 87 deletions(-) diff --git a/Sources/SwiftNetKit/BaseRequest.swift b/Sources/SwiftNetKit/BaseRequest.swift index 6224b40..bfdaf46 100644 --- a/Sources/SwiftNetKit/BaseRequest.swift +++ b/Sources/SwiftNetKit/BaseRequest.swift @@ -29,38 +29,4 @@ public struct BaseRequest: RequestProtocol { self.headers = headers self.body = body } - - func buildURLRequest() -> URLRequest { - var urlRequest = URLRequest(url: self.url) - - if let parameters = self.parameters { - let queryItems = parameters.map { key, value in - URLQueryItem(name: key, value: "\(value)") - } - var urlComponents = URLComponents(url: self.url, resolvingAgainstBaseURL: false) - urlComponents?.queryItems = queryItems - urlRequest.url = urlComponents?.url - } - - urlRequest.httpMethod = self.method.rawValue - urlRequest.allHTTPHeaderFields = self.headers - - if let body = self.body { - switch body { - case .data(let data): - urlRequest.httpBody = data - case .string(let string): - urlRequest.httpBody = string.data(using: .utf8) - case .jsonEncodable(let encodable): - let jsonData = try? JSONEncoder().encode(encodable) - urlRequest.httpBody = jsonData - - if headers?["Content-Type"] == nil { - urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") - } - } - } - - return urlRequest - } } diff --git a/Sources/SwiftNetKit/Models/NetworkError.swift b/Sources/SwiftNetKit/Models/NetworkError.swift index c692e0d..e5bd749 100644 --- a/Sources/SwiftNetKit/Models/NetworkError.swift +++ b/Sources/SwiftNetKit/Models/NetworkError.swift @@ -10,4 +10,5 @@ public enum NetworkError: Error { case decodingFailed case serverError(statusCode: Int) case requestFailed(error: Error) + case unknown } diff --git a/Sources/SwiftNetKit/NetworkService.swift b/Sources/SwiftNetKit/NetworkService.swift index eff9c97..1a9eca1 100644 --- a/Sources/SwiftNetKit/NetworkService.swift +++ b/Sources/SwiftNetKit/NetworkService.swift @@ -11,71 +11,112 @@ public struct NetworkService: NetworkServiceProtocol { internal let session: URLSession - public init(configuration: SessionConfiguration = .default) { + public init(configuration: SessionConfiguration = .default, timeoutInterval: TimeInterval? = nil) { + let sessionConfiguration: URLSessionConfiguration switch configuration { case .default: - self.session = URLSession(configuration: .default) + sessionConfiguration = URLSessionConfiguration.default case .ephemeral: - self.session = URLSession(configuration: .ephemeral) + sessionConfiguration = URLSessionConfiguration.ephemeral case .background(let identifier): - self.session = URLSession(configuration: .background(withIdentifier: identifier)) + sessionConfiguration = URLSessionConfiguration.background(withIdentifier: identifier) } + + if let timeoutInterval = timeoutInterval { + sessionConfiguration.timeoutIntervalForRequest = timeoutInterval + sessionConfiguration.timeoutIntervalForResource = timeoutInterval + } + + self.session = URLSession(configuration: sessionConfiguration) } - func start(_ request: Request) async throws -> Request.ResponseType { - do { - let urlRequest = request.buildURLRequest() - - let (data, response) = try await session.data(for: urlRequest) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard (200..<300).contains(httpResponse.statusCode) else { - throw NetworkError.serverError(statusCode: httpResponse.statusCode) - } - + func start( + _ request: Request, + retries: Int = 0, + retryInterval: TimeInterval = 1.0 + ) async throws -> Request.ResponseType { + var currentAttempt = 0 + var lastError: Error? + + while currentAttempt <= retries { do { - let decodedObject = try JSONDecoder().decode(Request.ResponseType.self, from: data) - return decodedObject + let urlRequest = request.buildURLRequest() + + let (data, response) = try await session.data(for: urlRequest) + + guard let httpResponse = response as? HTTPURLResponse else { + throw NetworkError.invalidResponse + } + + guard (200..<300).contains(httpResponse.statusCode) else { + throw NetworkError.serverError(statusCode: httpResponse.statusCode) + } + + do { + let decodedObject = try JSONDecoder().decode(Request.ResponseType.self, from: data) + return decodedObject + } catch { + throw NetworkError.decodingFailed + } } catch { - throw NetworkError.decodingFailed + lastError = error + currentAttempt += 1 + if currentAttempt <= retries { + try await Task.sleep(nanoseconds: UInt64(retryInterval * 1_000_000_000)) + } } - } catch { - throw NetworkError.requestFailed(error: error) } + + throw NetworkError.requestFailed(error: lastError ?? NetworkError.unknown) } - func start(_ request: Request, completion: @escaping (Result) -> Void) { - let urlRequest = request.buildURLRequest() + func start( + _ request: Request, + retries: Int = 0, + retryInterval: TimeInterval = 1.0, + completion: @escaping (Result) -> Void + ) { + var currentAttempt = 0 - session.dataTask(with: urlRequest) { data, response, error in - if let error = error { - completion(.failure(NetworkError.requestFailed(error: error))) - return - } - - guard let httpResponse = response as? HTTPURLResponse else { - completion(.failure(NetworkError.invalidResponse)) - return - } - - guard (200..<300).contains(httpResponse.statusCode) else { - completion(.failure(NetworkError.serverError(statusCode: httpResponse.statusCode))) - return - } + func attempt() { + let urlRequest = request.buildURLRequest() - if let data = data { - do { - let decodedObject = try JSONDecoder().decode(Request.ResponseType.self, from: data) - completion(.success(decodedObject)) - } catch { - completion(.failure(NetworkError.decodingFailed)) + session.dataTask(with: urlRequest) { data, response, error in + if let error = error { + if currentAttempt < retries { + currentAttempt += 1 + DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) { + attempt() + } + } else { + completion(.failure(NetworkError.requestFailed(error: error))) + } + return } - } else { - completion(.failure(NetworkError.invalidResponse)) - } - }.resume() + + guard let httpResponse = response as? HTTPURLResponse else { + completion(.failure(NetworkError.invalidResponse)) + return + } + + guard (200..<300).contains(httpResponse.statusCode) else { + completion(.failure(NetworkError.serverError(statusCode: httpResponse.statusCode))) + return + } + + if let data = data { + do { + let decodedObject = try JSONDecoder().decode(Request.ResponseType.self, from: data) + completion(.success(decodedObject)) + } catch { + completion(.failure(NetworkError.decodingFailed)) + } + } else { + completion(.failure(NetworkError.invalidResponse)) + } + }.resume() + } + + attempt() } } diff --git a/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift b/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift index 0d08a64..dcb6a57 100644 --- a/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift +++ b/Sources/SwiftNetKit/Protocols/NetworkServiceProtocol.swift @@ -10,9 +10,17 @@ import Foundation protocol NetworkServiceProtocol { var session: URLSession { get } - // Async/Await - func start(_ request: Request) async throws -> Request.ResponseType + // Async / Await + func start( + _ request: Request, + retries: Int, + retryInterval: TimeInterval + ) async throws -> Request.ResponseType // Completion Closure - func start(_ request: Request, completion: @escaping (Result) -> Void) + func start( + _ request: Request, retries: Int, + retryInterval: TimeInterval, + completion: @escaping (Result) -> Void + ) } diff --git a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift index feda3b2..fd4ae7a 100644 --- a/Sources/SwiftNetKit/Protocols/RequestProtocol.swift +++ b/Sources/SwiftNetKit/Protocols/RequestProtocol.swift @@ -18,3 +18,39 @@ protocol RequestProtocol { func buildURLRequest() -> URLRequest } + +extension RequestProtocol { + func buildURLRequest() -> URLRequest { + var urlRequest = URLRequest(url: self.url) + + if let parameters = self.parameters { + let queryItems = parameters.map { key, value in + URLQueryItem(name: key, value: "\(value)") + } + var urlComponents = URLComponents(url: self.url, resolvingAgainstBaseURL: false) + urlComponents?.queryItems = queryItems + urlRequest.url = urlComponents?.url + } + + urlRequest.httpMethod = self.method.rawValue + urlRequest.allHTTPHeaderFields = self.headers + + if let body = self.body { + switch body { + case .data(let data): + urlRequest.httpBody = data + case .string(let string): + urlRequest.httpBody = string.data(using: .utf8) + case .jsonEncodable(let encodable): + let jsonData = try? JSONEncoder().encode(encodable) + urlRequest.httpBody = jsonData + + if headers?["Content-Type"] == nil { + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + } + } + + return urlRequest + } +}