Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 158 additions & 15 deletions Sources/NextcloudKit/NKError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SwiftyJSON
import SwiftyXMLParser

typealias OCSPath = Array<String>

protocol DataSubscriptable {
subscript(path: OCSPath) -> Self { get }
}
Expand All @@ -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
Expand All @@ -57,91 +59,180 @@ 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
self.error = error
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
self.error = nsError
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
Expand All @@ -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
Expand All @@ -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<T: AFResponse>(error: AFError?, afResponse: T, responseData: Data? = nil) {
if let errorCode = afResponse.response?.statusCode {
guard let dataResponse = afResponse as? Alamofire.DataResponse<T.Success, T.Failure>,
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
}

Expand All @@ -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)
}
Expand All @@ -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
}
}
Expand Down
Loading