diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 8a1e08f8..7ccef8f7 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -9,6 +9,7 @@ import SwiftyJSON import SwiftyXMLParser typealias OCSPath = Array + protocol DataSubscriptable { subscript(path: OCSPath) -> Self { get } } @@ -34,6 +35,7 @@ extension OCSPath { public struct NKError: Error, Equatable, Sendable { static let internalError = -9999 + public let errorCode: Int public let errorDescription: String public let error: Error @@ -57,77 +59,155 @@ public struct NKError: Error, Equatable, Sendable { public static let success = NKError(errorCode: 0, errorDescription: "") + /// Returns a localized user-facing description for a known error code. + /// + /// - Parameter code: The HTTP, URL loading, WebDAV, OCS, or internal error code. + /// - Returns: A localized description when the code is known, otherwise `nil`. public static func getErrorDescription(for code: Int) -> String? { switch code { case -9999: return NSLocalizedString("_internal_server_", value: "Internal error", comment: "") + case -1001: return NSLocalizedString("_time_out_", value: "Time out", comment: "") + case -1004: return NSLocalizedString("_server_down_", value: "The server appears to be down", comment: "") + case -1005: return NSLocalizedString("_not_possible_connect_to_server_", value: "It is not possible to connect to the server at this time", comment: "") + case -1009: return NSLocalizedString("_not_connected_internet_", value: "Server connection error", comment: "") + case -1011: return NSLocalizedString("_error_", value: "Generic error", comment: "") + case -1012: return NSLocalizedString("_not_possible_connect_to_server_", value: "It is not possible to connect to the server at this time", comment: "") + case -1013: return NSLocalizedString("_user_authentication_required_", value: "User authentication required", comment: "") + case -1200: return NSLocalizedString("_ssl_connection_error_", value: "Connection SSL error, try again", comment: "") + case -1202: return NSLocalizedString("_ssl_certificate_untrusted_", value: "The certificate for this server is invalid", comment: "") - case 0: return "" + + case 0: + return "" + case 101: return NSLocalizedString("_forbidden_characters_from_server_", value: "The name contains at least one invalid character", comment: "") + + case 200: + return NSLocalizedString("_transfer_stopped_", value: "Transfer stopped", comment: "") + + case 207: + return NSLocalizedString("_error_multi_status_", value: "WebDAV multistatus", comment: "") + case 304: return NSLocalizedString("_error_not_modified_", value: "Resource not modified", comment: "") + case 400: return NSLocalizedString("_bad_request_", value: "Bad request", comment: "") + case 401: return NSLocalizedString("_unauthorized_", value: "Unauthorized", comment: "") + case 403: return NSLocalizedString("_error_not_permission_", value: "You don't have permission to complete the operation", comment: "") + case 404: return NSLocalizedString("_error_not_found_", value: "The requested resource could not be found", comment: "") + case 405: return NSLocalizedString("_method_not_allowed_", value: "The requested method is not supported", comment: "") + + case 408: + return NSLocalizedString("_request_timeout_", value: "Request timeout", comment: "") + case 409: return NSLocalizedString("_error_conflict_", value: "The request could not be completed due to a conflict with the current state of the resource", comment: "") + case 412: return NSLocalizedString("_error_precondition_", value: "The server does not meet one of the preconditions that the requester", comment: "") + case 413: return NSLocalizedString("_request_entity_too_large_", value: "The file is too large", comment: "") + case 417: return NSLocalizedString("_expectation_failed_", value: "Expectation failed", comment: "") + case 423: return NSLocalizedString("_webdav_locked_", value: "WebDAV Locked: Trying to access locked resource", comment: "") + + case 429: + return NSLocalizedString("_too_many_requests_", value: "Too many requests", comment: "") + case 500: return NSLocalizedString("_internal_server_", value: "Internal server error", comment: "") + + case 502: + return NSLocalizedString("_bad_gateway_", value: "Bad gateway", comment: "") + case 503: return NSLocalizedString("_server_maintenance_mode_", value: "Server is currently in maintenance mode", comment: "") + + case 504: + return NSLocalizedString("_gateway_timeout_", value: "Gateway timeout", comment: "") + case 507: return NSLocalizedString("_user_over_quota_", value: "Storage quota is reached", comment: "") - case 200: - return NSLocalizedString("_transfer_stopped_", value: "Transfer stopped", comment: "") - case 207: - return NSLocalizedString("_error_multi_status_", value: "WebDAV multistatus", comment: "") + case NSURLErrorCannotDecodeContentData: return NSLocalizedString("_invalid_data_format_", value: "Invalid data format", comment: "") + default: return nil } } + /// Returns a clean fallback description for an HTTP status code. + /// + /// This method intentionally avoids `HTTPURLResponse.description`, because that value contains + /// the full response dump, including URL and headers, and is not suitable for UI. + /// + /// - Parameter statusCode: The HTTP status code. + /// - Returns: A clean fallback description. + private static func httpFallbackDescription(for statusCode: Int) -> String { + let description = HTTPURLResponse.localizedString(forStatusCode: statusCode) + + if description.isEmpty { + return NSLocalizedString("_error_", value: "Generic error", comment: "") + } + + return description + } + + /// Creates an `NKError` from an explicit code and description. + /// + /// - Parameters: + /// - errorCode: The error code. + /// - errorDescription: The user-facing error description. + /// - responseData: Optional raw response data associated with the error. public init(errorCode: Int = 0, errorDescription: String = "", responseData: Data? = nil) { self.errorCode = errorCode self.errorDescription = errorDescription - self.error = NSError(domain: NSCocoaErrorDomain, code: self.errorCode, userInfo: [NSLocalizedDescriptionKey: self.errorDescription]) + self.error = NSError( + domain: NSCocoaErrorDomain, + code: self.errorCode, + userInfo: [NSLocalizedDescriptionKey: self.errorDescription] + ) self.responseData = responseData } + /// Creates an `NKError` from a generic Swift `Error`. + /// + /// - Parameters: + /// - error: The source error. + /// - responseData: Optional raw response data associated with the error. public init(error: Error, responseData: Data? = nil) { self.errorCode = error._code self.errorDescription = error.localizedDescription @@ -135,6 +215,11 @@ public struct NKError: Error, Equatable, Sendable { self.responseData = responseData } + /// Creates an `NKError` from an `NSError`. + /// + /// - Parameters: + /// - nsError: The source `NSError`. + /// - responseData: Optional raw response data associated with the error. public init(nsError: NSError, responseData: Data? = nil) { self.errorCode = nsError.code self.errorDescription = nsError.localizedDescription @@ -142,6 +227,12 @@ public struct NKError: Error, Equatable, Sendable { self.responseData = responseData } + /// Creates an `NKError` from an OCS JSON response. + /// + /// - Parameters: + /// - rootJson: The parsed JSON response. + /// - fallbackStatusCode: The fallback HTTP status code used when the OCS status code is missing. + /// - responseData: Optional raw response data associated with the error. public init(rootJson: JSON, fallbackStatusCode: Int?, responseData: Data? = nil) { let statuscode = rootJson[.ocsMetaCode].int ?? fallbackStatusCode ?? NSURLErrorCannotDecodeContentData errorCode = 200..<300 ~= statuscode ? 0 : statuscode @@ -151,23 +242,54 @@ public struct NKError: Error, Equatable, Sendable { } else if let metaMsg = rootJson[.ocsMetaMsg].string { errorDescription = metaMsg } else { - errorDescription = NKError.getErrorDescription(for: statuscode) ?? "" + errorDescription = NKError.getErrorDescription(for: statuscode) ?? NKError.httpFallbackDescription(for: statuscode) } + self.responseData = responseData - self.error = NSError(domain: NSCocoaErrorDomain, code: self.errorCode, userInfo: [NSLocalizedDescriptionKey: self.errorDescription]) + self.error = NSError( + domain: NSCocoaErrorDomain, + code: self.errorCode, + userInfo: [NSLocalizedDescriptionKey: self.errorDescription] + ) } + /// Creates an `NKError` from an HTTP status code. + /// + /// - Parameters: + /// - statusCode: The HTTP status code. + /// - fallbackDescription: A clean fallback description used when the status code is unknown. + /// - responseData: Optional raw response data associated with the error. public init(statusCode: Int, fallbackDescription: String, responseData: Data? = nil) { self.errorCode = statusCode - self.errorDescription = "\(statusCode): " + (NKError.getErrorDescription(for: statusCode) ?? fallbackDescription) - self.error = NSError(domain: NSCocoaErrorDomain, code: self.errorCode, userInfo: [NSLocalizedDescriptionKey: self.errorDescription]) + + let description = NKError.getErrorDescription(for: statusCode) ?? fallbackDescription + self.errorDescription = "\(statusCode): \(description)" + + self.error = NSError( + domain: NSCocoaErrorDomain, + code: self.errorCode, + userInfo: [NSLocalizedDescriptionKey: self.errorDescription] + ) + self.responseData = responseData } + /// Creates an `NKError` from an HTTP response. + /// + /// - Parameter httpResponse: The source HTTP response. init(httpResponse: HTTPURLResponse) { - self.init(statusCode: httpResponse.statusCode, fallbackDescription: httpResponse.description) + self.init( + statusCode: httpResponse.statusCode, + fallbackDescription: Self.httpFallbackDescription(for: httpResponse.statusCode) + ) } + /// Creates an `NKError` from an OCS or WebDAV XML response. + /// + /// - Parameters: + /// - xmlData: The raw XML response data. + /// - fallbackStatusCode: The fallback HTTP status code used when the OCS status code is missing. + /// - responseData: Optional raw response data associated with the error. init(xmlData: Data, fallbackStatusCode: Int? = nil, responseData: Data? = nil) { let xml = XML.parse(xmlData) let statuscode = xml[.ocsMetaCode].int ?? fallbackStatusCode ?? NSURLErrorCannotDecodeContentData @@ -180,18 +302,33 @@ public struct NKError: Error, Equatable, Sendable { } else if let metaMsg = xml[.ocsXMLMsg].text { errorDescription = metaMsg } else { - errorDescription = NKError.getErrorDescription(for: statuscode) ?? "" + errorDescription = NKError.getErrorDescription(for: statuscode) ?? NKError.httpFallbackDescription(for: statuscode) } + self.responseData = responseData - self.error = NSError(domain: NSCocoaErrorDomain, code: self.errorCode, userInfo: [NSLocalizedDescriptionKey: self.errorDescription]) + self.error = NSError( + domain: NSCocoaErrorDomain, + code: self.errorCode, + userInfo: [NSLocalizedDescriptionKey: self.errorDescription] + ) } + /// Creates an `NKError` from an Alamofire response and optional Alamofire error. + /// + /// - Parameters: + /// - error: The Alamofire error, if available. + /// - afResponse: The Alamofire response. + /// - responseData: Optional raw response data associated with the error. public init(error: AFError?, afResponse: T, responseData: Data? = nil) { if let errorCode = afResponse.response?.statusCode { guard let dataResponse = afResponse as? Alamofire.DataResponse, let errorData = dataResponse.data else { - self.init(statusCode: errorCode, fallbackDescription: afResponse.response?.description ?? "", responseData: responseData) + self.init( + statusCode: errorCode, + fallbackDescription: Self.httpFallbackDescription(for: errorCode), + responseData: responseData + ) return } @@ -205,14 +342,19 @@ public struct NKError: Error, Equatable, Sendable { switch error { case .createUploadableFailed(let error as NSError): self.init(nsError: error, responseData: responseData) + case .createURLRequestFailed(let error as NSError): self.init(nsError: error, responseData: responseData) + case .requestAdaptationFailed(let error as NSError): self.init(nsError: error, responseData: responseData) + case .sessionInvalidated(let error as NSError): self.init(nsError: error, responseData: responseData) + case .sessionTaskFailed(let error as NSError): self.init(nsError: error, responseData: responseData) + default: self.init(error: error, responseData: responseData) } @@ -227,8 +369,9 @@ public struct NKError: Error, Equatable, Sendable { public static func == (lhs: NKError, rhs: NKError?) -> Bool { if let rhs { - return lhs == rhs; + return lhs == rhs } + return false } }