initial commit
This commit is contained in:
commit
8de962eeb8
44 changed files with 4843 additions and 0 deletions
87
APIError.swift
Normal file
87
APIError.swift
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import Foundation
|
||||
|
||||
/// Typed errors surfaced by `APIClient`. HTTP status codes from the MCMS docs
|
||||
/// are mapped to specific cases so the UI can react (e.g. re-auth on `.unauthorized`).
|
||||
enum APIError: Error, Equatable {
|
||||
/// No usable base URL / malformed endpoint.
|
||||
case invalidURL
|
||||
/// The request never produced an HTTP response (offline, host unreachable, VPN off).
|
||||
case transport(message: String)
|
||||
/// TLS trust evaluation failed for the server.
|
||||
case serverTrust
|
||||
/// Could not encode the request body.
|
||||
case encoding(message: String)
|
||||
/// Could not decode the response body into the expected type.
|
||||
case decoding(message: String)
|
||||
|
||||
/// 400 — request/validation error. `details` carries the server payload.
|
||||
case badRequest(details: JSONValue?)
|
||||
/// 401 — not authenticated (failed login or expired session). Trigger re-login.
|
||||
case unauthorized
|
||||
/// 403 — authenticated but not permitted, OR a missing/invalid CSRF token.
|
||||
case forbidden(details: JSONValue?)
|
||||
/// 404 — resource (URL, file, or document) not found.
|
||||
case notFound
|
||||
/// 409 — ID already in use (uploads / creates).
|
||||
case conflict(details: JSONValue?)
|
||||
/// 415 — unsupported Content-Type.
|
||||
case unsupportedMediaType
|
||||
/// 500 — generic server error.
|
||||
case server(details: JSONValue?)
|
||||
|
||||
/// The API returned HTTP 2xx but `status == "fail"`.
|
||||
case apiFailure(details: JSONValue?)
|
||||
/// Any other unexpected status code.
|
||||
case unexpectedStatus(code: Int, details: JSONValue?)
|
||||
|
||||
/// Maps an HTTP status code (with optional decoded `details`) to a case.
|
||||
static func from(statusCode: Int, details: JSONValue?) -> APIError {
|
||||
switch statusCode {
|
||||
case 400: return .badRequest(details: details)
|
||||
case 401: return .unauthorized
|
||||
case 403: return .forbidden(details: details)
|
||||
case 404: return .notFound
|
||||
case 409: return .conflict(details: details)
|
||||
case 415: return .unsupportedMediaType
|
||||
case 500: return .server(details: details)
|
||||
default: return .unexpectedStatus(code: statusCode, details: details)
|
||||
}
|
||||
}
|
||||
|
||||
/// True when the right response is to send the user back to the login screen.
|
||||
/// Only 401 (expired/invalid session) qualifies — a 403 is usually a
|
||||
/// permission or CSRF issue where re-login won't help, so it must NOT bounce
|
||||
/// the user out mid-action.
|
||||
var requiresReauthentication: Bool {
|
||||
switch self {
|
||||
case .unauthorized: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension APIError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL: return "The request URL was invalid."
|
||||
case .transport(let m): return "Network error: \(m)"
|
||||
case .serverTrust: return "The server's TLS certificate could not be trusted."
|
||||
case .encoding(let m): return "Failed to encode request: \(m)"
|
||||
case .decoding(let m): return "Failed to read server response: \(m)"
|
||||
case .badRequest(let d): return "Request rejected (400). \(Self.detailText(d))"
|
||||
case .unauthorized: return "Not authenticated (401). Please sign in again."
|
||||
case .forbidden(let d): return "Forbidden (403) — your account may not have permission for this action. \(Self.detailText(d))"
|
||||
case .notFound: return "Resource not found (404)."
|
||||
case .conflict(let d): return "Conflict (409) — ID already in use. \(Self.detailText(d))"
|
||||
case .unsupportedMediaType: return "Unsupported media type (415)."
|
||||
case .server(let d): return "Server error (500). \(Self.detailText(d))"
|
||||
case .apiFailure(let d): return "Request failed. \(Self.detailText(d))"
|
||||
case .unexpectedStatus(let c, let d): return "Unexpected response (\(c)). \(Self.detailText(d))"
|
||||
}
|
||||
}
|
||||
|
||||
private static func detailText(_ details: JSONValue?) -> String {
|
||||
guard let details else { return "" }
|
||||
return details.compactDescription
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue