summaryrefslogtreecommitdiff
path: root/Hutch/Networking
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch/Networking')
-rw-r--r--Hutch/Networking/SRHTClient.swift33
-rw-r--r--Hutch/Networking/SRHTError.swift88
-rw-r--r--Hutch/Networking/SystemStatusRepository.swift51
3 files changed, 97 insertions, 75 deletions
diff --git a/Hutch/Networking/SRHTClient.swift b/Hutch/Networking/SRHTClient.swift
index 1b8b9b3..7e1f619 100644
--- a/Hutch/Networking/SRHTClient.swift
+++ b/Hutch/Networking/SRHTClient.swift
@@ -89,19 +89,12 @@ final class SRHTClient: Sendable {
throw SRHTError.unauthorized
}
if !(200...299).contains(http.statusCode) {
- // Try to extract GraphQL errors from the response body even on non-2xx
- if let gqlResponse = try? decoder.decode(GraphQLResponse<EmptyData>.self, from: data),
- let errors = gqlResponse.errors, !errors.isEmpty {
- throw SRHTError.graphQLErrors(errors)
- }
+ try throwGraphQLErrorsIfPresent(in: data)
throw SRHTError.httpError(http.statusCode)
}
}
- if let errorEnvelope = try? decoder.decode(GraphQLResponse<EmptyData>.self, from: data),
- let errors = errorEnvelope.errors, !errors.isEmpty {
- throw SRHTError.graphQLErrors(errors)
- }
+ try throwGraphQLErrorsIfPresent(in: data)
// Decode GraphQL response envelope
let graphQLResponse: GraphQLResponse<T>
@@ -249,14 +242,12 @@ final class SRHTClient: Sendable {
throw SRHTError.unauthorized
}
if !(200...299).contains(http.statusCode) {
+ try throwGraphQLErrorsIfPresent(in: data)
throw SRHTError.httpError(http.statusCode)
}
}
- if let errorEnvelope = try? decoder.decode(GraphQLResponse<EmptyData>.self, from: data),
- let errors = errorEnvelope.errors, !errors.isEmpty {
- throw SRHTError.graphQLErrors(errors)
- }
+ try throwGraphQLErrorsIfPresent(in: data)
let graphQLResponse: GraphQLResponse<T>
do {
@@ -381,14 +372,13 @@ final class SRHTClient: Sendable {
throw SRHTError.unauthorized
}
if !(200...299).contains(http.statusCode) {
- if let gqlResponse = try? decoder.decode(GraphQLResponse<EmptyData>.self, from: data),
- let errors = gqlResponse.errors, !errors.isEmpty {
- throw SRHTError.graphQLErrors(errors)
- }
+ try throwGraphQLErrorsIfPresent(in: data)
throw SRHTError.httpError(http.statusCode)
}
}
+ try throwGraphQLErrorsIfPresent(in: data)
+
let graphQLResponse: GraphQLResponse<T>
do {
graphQLResponse = try decoder.decode(GraphQLResponse<T>.self, from: data)
@@ -474,6 +464,7 @@ final class SRHTClient: Sendable {
throw SRHTError.unauthorized
}
if !(200...299).contains(http.statusCode) {
+ try throwGraphQLErrorsIfPresent(in: data)
throw SRHTError.httpError(http.statusCode)
}
}
@@ -627,6 +618,14 @@ final class SRHTClient: Sendable {
// MARK: - Data Helper
private extension SRHTClient {
+ func throwGraphQLErrorsIfPresent(in data: Data) throws {
+ if let envelope = try? decoder.decode(GraphQLResponse<EmptyData>.self, from: data),
+ let errors = envelope.errors,
+ !errors.isEmpty {
+ throw SRHTError.graphQLErrors(errors)
+ }
+ }
+
static func isTrustedAuthenticatedTextURL(_ url: URL) -> Bool {
guard url.scheme?.localizedCaseInsensitiveCompare("https") == .orderedSame,
let host = url.host?.lowercased() else {
diff --git a/Hutch/Networking/SRHTError.swift b/Hutch/Networking/SRHTError.swift
index b9fc4c8..e946774 100644
--- a/Hutch/Networking/SRHTError.swift
+++ b/Hutch/Networking/SRHTError.swift
@@ -18,7 +18,7 @@ enum SRHTError: LocalizedError, Sendable {
var errorDescription: String? {
switch self {
case .graphQLErrors(let errors):
- let messages = errors.map(\.message).joined(separator: "\n")
+ let messages = errors.diagnosticSummary
return "GraphQL error: \(messages)"
case .httpError(let code):
return "Server returned HTTP \(code)."
@@ -33,17 +33,21 @@ enum SRHTError: LocalizedError, Sendable {
}
}
- var userFacingMessage: String {
+ nonisolated var userFacingMessage: String {
switch self {
case .graphQLErrors(let errors):
- let firstMessage = errors.first?.message.lowercased() ?? ""
- if firstMessage.contains("unauthorized") || firstMessage.contains("forbidden") {
+ switch errors.classification {
+ case .unauthorized, .forbidden:
return "You do not have permission to do that."
- }
- if firstMessage.contains("not found") || firstMessage.contains("no rows in result set") {
+ case .notFound, .noRows, .missingReference, .unknownRevision:
return "That content is no longer available."
+ case .serviceNotProvisioned:
+ return "That account needs to activate this SourceHut service before this action can succeed."
+ case .validation:
+ return errors.primaryMessage ?? "Please review your changes and try again."
+ case .other:
+ return "Something went wrong. Please try again."
}
- return "Something went wrong. Please try again."
case .httpError(let code):
if code == 401 {
return "Please sign in again."
@@ -105,7 +109,7 @@ enum SRHTError: LocalizedError, Sendable {
}
extension Error {
- var userFacingMessage: String {
+ nonisolated var userFacingMessage: String {
if let error = self as? SRHTError {
return error.userFacingMessage
}
@@ -125,6 +129,22 @@ extension Error {
return "Something went wrong. Please try again."
}
}
+
+ nonisolated var graphQLErrors: [GraphQLError]? {
+ guard let srhtError = self as? SRHTError,
+ case let SRHTError.graphQLErrors(errors) = srhtError else {
+ return nil
+ }
+ return errors
+ }
+
+ nonisolated func matchesGraphQLErrorClassification(_ classification: GraphQLErrorClassification) -> Bool {
+ graphQLErrors?.classification == classification
+ }
+
+ nonisolated func containsGraphQLErrorMessage(_ fragment: String) -> Bool {
+ graphQLErrors?.containsMessage(fragment) == true
+ }
}
/// A single error entry from the GraphQL `errors` array.
@@ -133,6 +153,58 @@ struct GraphQLError: Decodable, Sendable {
let locations: [GraphQLErrorLocation]?
}
+enum GraphQLErrorClassification: Sendable {
+ case unauthorized
+ case forbidden
+ case notFound
+ case noRows
+ case missingReference
+ case unknownRevision
+ case serviceNotProvisioned
+ case validation
+ case other
+}
+
+extension Array where Element == GraphQLError {
+ nonisolated var classification: GraphQLErrorClassification {
+ if containsMessage("unauthorized") { return .unauthorized }
+ if containsMessage("forbidden") { return .forbidden }
+ if containsMessage("reference not found") { return .missingReference }
+ if containsMessage("no rows in result set") { return .noRows }
+ if containsMessage("unknown revision") || containsMessage("path not in the working tree") {
+ return .unknownRevision
+ }
+ if containsMessage("not found") || containsMessage("no such") || containsMessage("missing revision") {
+ return .notFound
+ }
+ if containsMessage("no such repository or user found") {
+ return .serviceNotProvisioned
+ }
+ if let primaryMessage, !primaryMessage.isEmpty {
+ return .validation
+ }
+ return .other
+ }
+
+ nonisolated var primaryMessage: String? {
+ let candidates = map(\.message)
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+ .filter { !$0.isEmpty }
+ return candidates.first
+ }
+
+ nonisolated var diagnosticSummary: String {
+ map(\.message)
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+ .filter { !$0.isEmpty }
+ .joined(separator: "\n")
+ }
+
+ nonisolated func containsMessage(_ fragment: String) -> Bool {
+ contains { $0.message.localizedCaseInsensitiveContains(fragment) }
+ }
+}
+
struct GraphQLErrorLocation: Decodable, Sendable {
let line: Int
let column: Int
diff --git a/Hutch/Networking/SystemStatusRepository.swift b/Hutch/Networking/SystemStatusRepository.swift
index 5f51315..028c64e 100644
--- a/Hutch/Networking/SystemStatusRepository.swift
+++ b/Hutch/Networking/SystemStatusRepository.swift
@@ -130,56 +130,7 @@ actor SystemStatusRepository {
}
private func refreshErrorMessage(from error: any Error) -> String {
- if let error = error as? SRHTError {
- switch error {
- 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 underlyingError):
- return refreshErrorMessage(from: underlyingError)
- case .unauthorized:
- return "Please sign in again."
- }
- }
-
- 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 "Something went wrong. Please try again."
- }
+ error.userFacingMessage
}
}