Skip to content

Instantly share code, notes, and snippets.

@robertvunabandi
Created November 18, 2025 04:54
Show Gist options
  • Select an option

  • Save robertvunabandi/832881cfe299568e6732dcca4236de00 to your computer and use it in GitHub Desktop.

Select an option

Save robertvunabandi/832881cfe299568e6732dcca4236de00 to your computer and use it in GitHub Desktop.
//
// 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