// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Foundation /// Represents a Google Cloud API error response body as defined by AIP-193. @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) package struct GoogleCloudAPIError: Codable, Sendable, Equatable, Hashable { /// Represents the nested error `Status` in a Google Cloud API error. /// /// See [AIP-193](https://google.aip.dev/193#http11json-representation) for more details. private struct Status: Codable, Sendable, Equatable, Hashable { /// The HTTP status code value. let code: Int /// A developer-facing, human-readable English error message. let message: String /// The canonical status code indicating the nature of the error. let status: RPCErrorStatus? /// Additional details about the error. let details: [Detail]? } /// The HTTP status code value. package var code: Int { error.code } /// A developer-facing, human-readable English error message. package var message: String { error.message } /// The canonical status code indicating the nature of the error. package var status: RPCErrorStatus? { error.status } /// Additional details about the error. package var details: [Detail]? { error.details } /// The retry delay duration, if provided by `RetryInfo` details or HTTP headers. package var retryDelay: Duration? { if let explicitRetryDelay { return explicitRetryDelay } if let details { for detail in details { if case .retryInfo(let retryInfo) = detail { return retryInfo.duration } } } return nil } private let error: Status private let explicitRetryDelay: Duration? /// Creates a new `GoogleCloudAPIError`. /// /// - Parameters: /// - code: The HTTP status code value. /// - message: A developer-facing, human-readable English error message. /// - status: The canonical status code indicating the nature of the error. /// - details: Additional details about the error. /// - retryDelay: Optional retry delay duration. package init( code: Int, message: String, status: RPCErrorStatus? = nil, details: [Detail]? = nil, retryDelay: Duration? = nil ) { self.error = Status(code: code, message: message, status: status, details: details) self.explicitRetryDelay = retryDelay } /// Returns a copy of this error with the specified retry delay duration. /// /// - Parameter retryDelay: The retry delay duration. /// - Returns: A new `GoogleCloudAPIError` instance with updated retry delay. package func withRetryDelay(_ retryDelay: Duration?) -> GoogleCloudAPIError { GoogleCloudAPIError( code: code, message: message, status: status, details: details, retryDelay: retryDelay ?? self.explicitRetryDelay ) } } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) extension GoogleCloudAPIError { /// Canonical gRPC/RPC error status codes. package enum RPCErrorStatus: Codable, Sendable, Equatable, Hashable { /// Client cancelled the request. case cancelled /// Unknown error. case unknown /// Client specified an invalid argument. case invalidArgument /// Deadline expired before operation could complete. case deadlineExceeded /// Some requested entity was not found. case notFound /// Some entity that we attempted to create already exists. case alreadyExists /// The caller does not have permission to execute the specified action. case permissionDenied /// Some resource has been exhausted. case resourceExhausted /// Operation was rejected because the system is not in a state required for its execution. case failedPrecondition /// The operation was aborted. case aborted /// Operation was attempted past the valid range. case outOfRange /// Operation is not implemented or not supported/enabled in this service. case unimplemented /// Internal errors. case internalError /// The service is currently unavailable. case unavailable /// Unrecoverable data loss or corruption. case dataLoss /// The request does not have valid authentication credentials for the operation. case unauthenticated /// Unrecognized placeholder for future compatibility. /// /// - Parameter value: The raw string value of the unrecognized enum case. case unrecognized(_ value: String) } /// Details attached to a Google Cloud API error. package enum Detail: Codable, Sendable, Equatable, Hashable { /// Primary machine-readable error information. case errorInfo(ErrorInfo) /// Localized human-readable error message. case localizedMessage(LocalizedMessage) /// Troubleshooting documentation links. case help(Help) /// Retry delay advice for rate-limited requests. case retryInfo(RetryInfo) /// Unrecognized detail payload. case unrecognized(JSONObject) private enum CodingKeys: String, CodingKey { case typeURL = "@type" } package init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let typeURL = try container.decodeIfPresent(String.self, forKey: .typeURL) switch typeURL { case "type.googleapis.com/google.rpc.ErrorInfo": self = .errorInfo(try ErrorInfo(from: decoder)) case "type.googleapis.com/google.rpc.LocalizedMessage": self = .localizedMessage(try LocalizedMessage(from: decoder)) case "type.googleapis.com/google.rpc.Help": self = .help(try Help(from: decoder)) case "type.googleapis.com/google.rpc.RetryInfo": self = .retryInfo(try RetryInfo(from: decoder)) default: self = .unrecognized(try JSONObject(from: decoder)) } } package func encode(to encoder: any Encoder) throws { switch self { case .errorInfo(let value): try value.encode(to: encoder) case .localizedMessage(let value): try value.encode(to: encoder) case .help(let value): try value.encode(to: encoder) case .retryInfo(let value): try value.encode(to: encoder) case .unrecognized(let value): try value.encode(to: encoder) } } } /// Machine-readable context about the error. package struct ErrorInfo: Codable, Sendable, Equatable, Hashable { /// The reason of the error. package let reason: String /// The logical grouping to which the reason belongs. package let domain: String /// Additional structured metadata. package let metadata: [String: String]? enum CodingKeys: String, CodingKey { case typeURL = "@type" case reason case domain case metadata } /// Creates a new `ErrorInfo`. /// /// - Parameters: /// - reason: The reason of the error. /// - domain: The logical grouping to which the reason belongs. /// - metadata: Additional structured metadata. package init(reason: String, domain: String, metadata: [String: String]? = nil) { self.reason = reason self.domain = domain self.metadata = metadata } package init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.reason = try container.decode(String.self, forKey: .reason) self.domain = try container.decode(String.self, forKey: .domain) self.metadata = try container.decodeIfPresent([String: String].self, forKey: .metadata) } package func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode("type.googleapis.com/google.rpc.ErrorInfo", forKey: .typeURL) try container.encode(reason, forKey: .reason) try container.encode(domain, forKey: .domain) try container.encodeIfPresent(metadata, forKey: .metadata) } } /// Localized error message for client consumption. package struct LocalizedMessage: Codable, Sendable, Equatable, Hashable { /// Locale of the message (e.g. "en-US"). package let locale: String /// The localized message text. package let message: String enum CodingKeys: String, CodingKey { case typeURL = "@type" case locale case message } /// Creates a new `LocalizedMessage`. /// /// - Parameters: /// - locale: Locale of the message (e.g. "en-US"). /// - message: The localized message text. package init(locale: String, message: String) { self.locale = locale self.message = message } package init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.locale = try container.decode(String.self, forKey: .locale) self.message = try container.decode(String.self, forKey: .message) } package func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode("type.googleapis.com/google.rpc.LocalizedMessage", forKey: .typeURL) try container.encode(locale, forKey: .locale) try container.encode(message, forKey: .message) } } /// Supplemental troubleshooting documentation. package struct Help: Codable, Sendable, Equatable, Hashable { /// A single documentation link. package struct Link: Codable, Sendable, Equatable, Hashable { /// Description of the link. package let description: String /// Absolute URL of the documentation. package let url: String enum CodingKeys: String, CodingKey { case description case url } /// Creates a new `Link`. /// /// - Parameters: /// - description: Description of the link. /// - url: Absolute URL of the documentation. package init(description: String, url: String) { self.description = description self.url = url } } /// Links to troubleshooting pages. package let links: [Link] enum CodingKeys: String, CodingKey { case typeURL = "@type" case links } /// Creates a new `Help`. /// /// - Parameters: /// - links: Links to troubleshooting pages. package init(links: [Link]) { self.links = links } package init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.links = try container.decode([Link].self, forKey: .links) } package func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode("type.googleapis.com/google.rpc.Help", forKey: .typeURL) try container.encode(links, forKey: .links) } } /// Information about when a client should retry a failed request. package struct RetryInfo: Codable, Sendable, Equatable, Hashable { /// The delay duration string (e.g. `"45.5s"`). package let retryDelay: String /// The delay converted to `Duration`, if parseable. package var duration: Duration? { var delayString = retryDelay.trimmingCharacters(in: .whitespaces) if delayString.hasSuffix("s") { delayString.removeLast() } guard let seconds = Double(delayString), seconds >= 0 else { return nil } return .seconds(seconds) } enum CodingKeys: String, CodingKey { case typeURL = "@type" case retryDelay } /// Creates a new `RetryInfo`. /// /// - Parameter retryDelay: The delay duration string (e.g. `"45.5s"`). package init(retryDelay: String) { self.retryDelay = retryDelay } package init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.retryDelay = try container.decode(String.self, forKey: .retryDelay) } package func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode("type.googleapis.com/google.rpc.RetryInfo", forKey: .typeURL) try container.encode(retryDelay, forKey: .retryDelay) } } } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) extension GoogleCloudAPIError.RPCErrorStatus: RawRepresentable { package var rawValue: String { switch self { case .cancelled: "CANCELLED" case .unknown: "UNKNOWN" case .invalidArgument: "INVALID_ARGUMENT" case .deadlineExceeded: "DEADLINE_EXCEEDED" case .notFound: "NOT_FOUND" case .alreadyExists: "ALREADY_EXISTS" case .permissionDenied: "PERMISSION_DENIED" case .resourceExhausted: "RESOURCE_EXHAUSTED" case .failedPrecondition: "FAILED_PRECONDITION" case .aborted: "ABORTED" case .outOfRange: "OUT_OF_RANGE" case .unimplemented: "UNIMPLEMENTED" case .internalError: "INTERNAL" case .unavailable: "UNAVAILABLE" case .dataLoss: "DATA_LOSS" case .unauthenticated: "UNAUTHENTICATED" case .unrecognized(let value): value } } package init(rawValue: String) { switch rawValue { case "CANCELLED": self = .cancelled case "UNKNOWN": self = .unknown case "INVALID_ARGUMENT": self = .invalidArgument case "DEADLINE_EXCEEDED": self = .deadlineExceeded case "NOT_FOUND": self = .notFound case "ALREADY_EXISTS": self = .alreadyExists case "PERMISSION_DENIED": self = .permissionDenied case "RESOURCE_EXHAUSTED": self = .resourceExhausted case "FAILED_PRECONDITION": self = .failedPrecondition case "ABORTED": self = .aborted case "OUT_OF_RANGE": self = .outOfRange case "UNIMPLEMENTED": self = .unimplemented case "INTERNAL": self = .internalError case "UNAVAILABLE": self = .unavailable case "DATA_LOSS": self = .dataLoss case "UNAUTHENTICATED": self = .unauthenticated default: self = .unrecognized(rawValue) } } } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) extension GoogleCloudAPIError: LocalizedError { package var errorDescription: String? { if let details { for detail in details { if case .localizedMessage(let localizedMessage) = detail { return localizedMessage.message } } } return message } package var failureReason: String? { if let details { for detail in details { if case .errorInfo(let errorInfo) = detail { return errorInfo.reason } } } return status?.rawValue } package var helpAnchor: String? { if let details { for detail in details { if case .help(let help) = detail, let firstLink = help.links.first { return firstLink.url } } } return nil } } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) extension GoogleCloudAPIError: CustomNSError { package static var errorDomain: String { "GoogleCloudAPIError" } package var errorCode: Int { code } package var errorUserInfo: [String: Any] { var userInfo = [String: Any]() userInfo["code"] = code if let status { userInfo["status"] = status.rawValue } if let retryDelay { userInfo["retryAfterSeconds"] = Double(retryDelay.components.seconds) } if let details { for detail in details { switch detail { case .errorInfo(let errorInfo): userInfo["domain"] = errorInfo.domain userInfo["reason"] = errorInfo.reason if let metadata = errorInfo.metadata { userInfo["metadata"] = metadata } case .help(let help): let links = help.links.map { ["description": $0.description, "url": $0.url] } userInfo["helpLinks"] = links case .localizedMessage(let localizedMessage): userInfo["locale"] = localizedMessage.locale case .retryInfo(let retryInfo): userInfo["retryDelay"] = retryInfo.retryDelay case .unrecognized(let obj): if var prevAdditional = userInfo["additionalDetails"] as? [JSONObject] { prevAdditional.append(obj) userInfo["additionalDetails"] = prevAdditional } else { userInfo["additionalDetails"] = [obj] } } } } return userInfo } }