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