Sources/Confidence/Http/NetworkClient.swift (132 lines of code) (raw):

import Foundation final class NetworkClient: HttpClient { private let headers: [String: String] private let retry: Retry private let session: URLSession private let baseUrl: String private var timeoutIntervalForRequests: Double public init( session: URLSession? = nil, baseUrl: String, defaultHeaders: [String: String] = [:], retry: Retry = .none, timeoutIntervalForRequests: Double ) { self.session = session ?? { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = defaultHeaders return URLSession(configuration: configuration) }() self.headers = defaultHeaders self.retry = retry self.baseUrl = baseUrl self.timeoutIntervalForRequests = timeoutIntervalForRequests } public func post<T: Decodable>( path: String, data: Encodable ) async throws -> HttpClientResult<T> { let request = try buildRequest(path: path, data: data) let requestResult = await perform(request: request, retry: self.retry) if let error = requestResult.error { return .failure(error) } guard let response = requestResult.httpResponse, let data = requestResult.data else { return .failure(ConfidenceError.internalError(message: "Bad response")) } do { let httpClientResult: HttpClientResponse<T> = try self.buildResponse(response: response, data: data) return .success(httpClientResult) } catch { return .failure(error) } } private func perform( request: URLRequest, retry: Retry ) async -> RequestResult { let retryHandler = retry.handler() let retryWait: TimeInterval? = retryHandler.retryIn() do { let (data, response) = try await self.session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { return RequestResult(httpResponse: nil, data: nil, error: HttpClientError.invalidResponse) } if self.shouldRetry(httpResponse: httpResponse), let retryWait { try? await Task.sleep(nanoseconds: UInt64(retryWait * 1_000_000_000)) return await self.perform(request: request, retry: retry) } return RequestResult(httpResponse: httpResponse, data: data, error: nil) } catch { if self.shouldRetry(error: error), let retryWait { try? await Task.sleep(nanoseconds: UInt64(retryWait * 1_000_000_000)) return await self.perform(request: request, retry: retry) } else { return RequestResult(httpResponse: nil, data: nil, error: error) } } } } struct RequestResult { var httpResponse: HTTPURLResponse? var data: Data? var error: Error? } // MARK: Private extension NetworkClient { private func constructURL(base: String, path: String) -> URL? { let normalisedBase = base.hasSuffix("/") ? base : "\(base)" let normalisedPath = path.hasPrefix("/") ? String(path.dropFirst()) : path return URL(string: "\(normalisedBase)\(normalisedPath)") } private func buildRequest(path: String, data: Encodable) throws -> URLRequest { guard let url = constructURL(base: baseUrl, path: path) else { throw ConfidenceError.internalError(message: "Could not create service url") } var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let jsonData = try encoder.encode(data) request.httpBody = jsonData return request } private func buildResponse<T: Decodable>( response httpURLResponse: HTTPURLResponse?, data: Data? ) throws -> HttpClientResponse<T> { guard let httpURLResponse else { throw ConfidenceError.internalError(message: "Invalid response") } var response: HttpClientResponse<T> = HttpClientResponse(response: httpURLResponse) if let responseData = data { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 if response.response.status == .ok { response.decodedData = try decoder.decode(T.self, from: responseData) } else { do { response.decodedError = try decoder.decode(HttpError.self, from: responseData) } catch { let message = String(data: responseData, encoding: .utf8) response.decodedError = HttpError( code: httpURLResponse.statusCode, message: message ?? "{Error when decoding error message}", details: [] ) } } } return response } private func shouldRetry(httpResponse: HTTPURLResponse) -> Bool { httpResponse.status?.responseType == .serverError } private func shouldRetry(error: Error) -> Bool { (error as? URLError)?.code == .timedOut } }