Created
November 18, 2025 04:54
-
-
Save robertvunabandi/832881cfe299568e6732dcca4236de00 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // NetworkRequest.swift | |
| // Reading Journal | |
| // | |
| // | |
| import Foundation | |
| extension Network { | |
| /// This represents a network request. This class is what does the work of | |
| /// making a Web call and getting the result from that call (including parsing | |
| /// that result). | |
| final class Request<Resource: NetworkResource, Result: Decodable> | |
| where Resource.Result == Result | |
| { | |
| let resource: Resource | |
| init(_ resource: Resource) { | |
| self.resource = resource | |
| } | |
| private let LOG = Tools.getLogger(filename: #file) | |
| } | |
| } | |
| extension Network.Request { | |
| /// Fetch the result from this request. | |
| /// **Don't call this directly:** the ``NetworkResource`` protocol has a | |
| /// `fetch` method that invokes this call. Use that instead. | |
| func fetch(debug: NetworkRequestDebugOption = .none) async throws -> Result { | |
| do { | |
| return try await fetch(resource.request(), debug: debug) | |
| } catch { | |
| if error is NetworkError { | |
| let newError = error as! NetworkError | |
| newError.log(LOG, resource: resource) | |
| throw newError | |
| } | |
| LOG.fatal("Unhandled error: \(error.localizedDescription)", #line) | |
| throw NetworkError.unhandledError | |
| } | |
| } | |
| /// Makes a fetch request for the given ``URLRequest`` and returns the | |
| /// result. If it returns `nil`, it means that there was an error with the request. | |
| private func fetch( | |
| _ request: URLRequest, | |
| debug: NetworkRequestDebugOption = .none | |
| ) | |
| async throws -> Result | |
| { | |
| URLSessionConfiguration.default.httpCookieAcceptPolicy = .always | |
| URLSessionConfiguration.ephemeral.httpCookieAcceptPolicy = .always | |
| let data: Data, response: URLResponse | |
| do { | |
| (data, response) = try await URLSession.shared.data(for: request) | |
| } catch { | |
| throw NetworkError.dataRequestFailed | |
| } | |
| // This will log only if the given debug object allows logging. | |
| debug.log(LOG, data: data, response: response) | |
| // Then, this is the primary way of parsing errors which are | |
| // explicit from the response. | |
| if let httpError = HttpError.from(response: response) { | |
| LOG.mustfix("[Response Error for \(resource.description)]", #line) | |
| switch httpError { | |
| case let .knownServerError(errorCode): | |
| LOG.warning("User Error (\(errorCode))", #line) | |
| throw NetworkError.userError | |
| case let .serverFailure(errorCode): | |
| LOG.mustfix("Server Failure (\(errorCode))", #line) | |
| throw NetworkError.serverFailure | |
| case let .invalidOrUnknownStatusCode(errorCode): | |
| LOG.mustfix("Invalid HTTP Status Code (\(errorCode))", #line) | |
| throw NetworkError.serverFailure | |
| case .invalidURLResponse: | |
| LOG.mustfix("Invalid URL Response", #line) | |
| throw NetworkError.serverFailure | |
| } | |
| } | |
| do { | |
| return try Network.JsonDecoder.decode(Result.self, from: data) | |
| } catch { | |
| LOG.warning("JSON Parsing Error for (\(resource.description))", #line) | |
| debug.logData(LOG, data, force: true) | |
| throw NetworkError.failedToParseResponse | |
| } | |
| } | |
| } | |
| private enum HttpError { | |
| /// Errors that are always handled by the server and therefore the data | |
| /// received should be decodable as an ApiErrorObject. | |
| case knownServerError(errorCode: Int) | |
| /// Server errors, 500 (internal server error) is indeed handled but the rest might | |
| /// not. So, data might not be decodable. | |
| case serverFailure(errorCode: Int) | |
| /// Some other errors that we're clearly not aware off. | |
| case invalidOrUnknownStatusCode(errorCode: Int) | |
| /// When we don't receive an `HTTPURLResponse` which could mean that | |
| /// something is off on the server. | |
| case invalidURLResponse | |
| /// Parse the `HttpError` from the `URLResponse` | |
| static func from(response: URLResponse) -> HttpError? { | |
| guard let response = response as? HTTPURLResponse else { | |
| return .invalidURLResponse | |
| } | |
| if (400 ... 499).contains(response.statusCode) { | |
| return .knownServerError(errorCode: response.statusCode) | |
| } | |
| if (500 ... 599).contains(response.statusCode) { | |
| return .serverFailure(errorCode: response.statusCode) | |
| } | |
| if !(200 ... 399).contains(response.statusCode) { | |
| return .invalidOrUnknownStatusCode(errorCode: response.statusCode) | |
| } | |
| return nil | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment