krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v2.10.3: Hutch/Networking/SRHTError.swift · raw

  1import Foundation
  2
  3/// Errors produced by the SourceHut GraphQL client.
  4enum SRHTError: LocalizedError, Sendable {
  5    /// The server returned one or more GraphQL-level errors.
  6    case graphQLErrors([GraphQLError])
  7    /// The HTTP response had a non-2xx status code.
  8    case httpError(Int)
  9    /// The client refused to send credentials to an unexpected URL.
 10    case invalidAuthenticatedURL(URL)
 11    /// The response data could not be decoded.
 12    case decodingError(any Error)
 13    /// A networking error from URLSession (timeout, DNS, connectivity, etc.).
 14    case networkError(any Error)
 15    /// 401 or no authentication token configured.
 16    case unauthorized
 17
 18    var errorDescription: String? {
 19        switch self {
 20        case .graphQLErrors(let errors):
 21            let messages = errors.map(\.message).joined(separator: "\n")
 22            return "GraphQL error: \(messages)"
 23        case .httpError(let code):
 24            return "Server returned HTTP \(code)."
 25        case .invalidAuthenticatedURL(let url):
 26            return "Refused to authenticate request to unexpected URL: \(url.absoluteString)"
 27        case .decodingError(let error):
 28            return "Failed to decode response: \(error.localizedDescription)"
 29        case .networkError(let error):
 30            return "Network error: \(error.localizedDescription)"
 31        case .unauthorized:
 32            return "Authentication required. Please sign in again."
 33        }
 34    }
 35
 36    var userFacingMessage: String {
 37        switch self {
 38        case .graphQLErrors(let errors):
 39            let firstMessage = errors.first?.message.lowercased() ?? ""
 40            if firstMessage.contains("unauthorized") || firstMessage.contains("forbidden") {
 41                return "You do not have permission to do that."
 42            }
 43            if firstMessage.contains("not found") || firstMessage.contains("no rows in result set") {
 44                return "That content is no longer available."
 45            }
 46            return "Something went wrong. Please try again."
 47        case .httpError(let code):
 48            if code == 401 {
 49                return "Please sign in again."
 50            }
 51            if code == 403 {
 52                return "You do not have permission to do that."
 53            }
 54            if code == 404 {
 55                return "That content is no longer available."
 56            }
 57            if (500...599).contains(code) {
 58                return "The server is unavailable right now. Please try again."
 59            }
 60            return "Something went wrong. Please try again."
 61        case .invalidAuthenticatedURL:
 62            return "That request could not be completed."
 63        case .decodingError:
 64            return "The response could not be loaded right now."
 65        case .networkError(let error):
 66            let nsError = error as NSError
 67            switch nsError.code {
 68            case NSURLErrorNotConnectedToInternet,
 69                 NSURLErrorNetworkConnectionLost,
 70                 NSURLErrorTimedOut,
 71                 NSURLErrorCannotFindHost,
 72                 NSURLErrorCannotConnectToHost,
 73                 NSURLErrorDNSLookupFailed,
 74                 NSURLErrorInternationalRoamingOff,
 75                 NSURLErrorDataNotAllowed:
 76                return "Check your connection and try again."
 77            default:
 78                return "The network request failed. Please try again."
 79            }
 80        case .unauthorized:
 81            return "Please sign in again."
 82        }
 83    }
 84
 85    /// Whether this error represents a connectivity issue (no internet, timeout, DNS).
 86    var isConnectivityError: Bool {
 87        switch self {
 88        case .networkError(let error):
 89            let nsError = error as NSError
 90            let connectivityCodes: Set<Int> = [
 91                NSURLErrorNotConnectedToInternet,
 92                NSURLErrorNetworkConnectionLost,
 93                NSURLErrorTimedOut,
 94                NSURLErrorCannotFindHost,
 95                NSURLErrorCannotConnectToHost,
 96                NSURLErrorDNSLookupFailed,
 97                NSURLErrorInternationalRoamingOff,
 98                NSURLErrorDataNotAllowed
 99            ]
100            return connectivityCodes.contains(nsError.code)
101        default:
102            return false
103        }
104    }
105}
106
107extension Error {
108    var userFacingMessage: String {
109        if let error = self as? SRHTError {
110            return error.userFacingMessage
111        }
112
113        let nsError = self as NSError
114        switch nsError.code {
115        case NSURLErrorNotConnectedToInternet,
116             NSURLErrorNetworkConnectionLost,
117             NSURLErrorTimedOut,
118             NSURLErrorCannotFindHost,
119             NSURLErrorCannotConnectToHost,
120             NSURLErrorDNSLookupFailed,
121             NSURLErrorInternationalRoamingOff,
122             NSURLErrorDataNotAllowed:
123            return "Check your connection and try again."
124        default:
125            return "Something went wrong. Please try again."
126        }
127    }
128}
129
130/// A single error entry from the GraphQL `errors` array.
131struct GraphQLError: Decodable, Sendable {
132    let message: String
133    let locations: [GraphQLErrorLocation]?
134}
135
136struct GraphQLErrorLocation: Decodable, Sendable {
137    let line: Int
138    let column: Int
139}