krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.6.1: 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}