Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FunctionsError #13601

Merged
merged 1 commit into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
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
18 changes: 7 additions & 11 deletions FirebaseFunctions/Sources/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -509,13 +509,13 @@ enum FunctionsConstants {
if let error = error as NSError? {
let localError: (any Error)?
if error.domain == kGTMSessionFetcherStatusDomain {
localError = FunctionsErrorCode.errorForResponse(
status: error.code,
localError = FunctionsError(
httpStatusCode: error.code,
body: data,
serializer: serializer
)
} else if error.domain == NSURLErrorDomain, error.code == NSURLErrorTimedOut {
localError = FunctionsErrorCode.deadlineExceeded.generatedError(userInfo: nil)
localError = FunctionsError(.deadlineExceeded)
} else {
localError = nil
}
Expand All @@ -525,15 +525,11 @@ enum FunctionsConstants {

// Case 2: `data` is `nil` -> always throws
guard let data else {
throw FunctionsErrorCode.internal.generatedError(userInfo: nil)
throw FunctionsError(.internal)
}

// Case 3: `data` is not `nil` but might specify a custom error -> throws conditionally
if let bodyError = FunctionsErrorCode.errorForResponse(
status: 200,
body: data,
serializer: serializer
) {
if let bodyError = FunctionsError(httpStatusCode: 200, body: data, serializer: serializer) {
throw bodyError
}

Expand All @@ -546,13 +542,13 @@ enum FunctionsConstants {

guard let responseJSON = responseJSONObject as? NSDictionary else {
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
throw FunctionsErrorCode.internal.generatedError(userInfo: userInfo)
throw FunctionsError(.internal, userInfo: userInfo)
}

// `result` is checked for backwards compatibility:
guard let dataJSON = responseJSON["data"] ?? responseJSON["result"] else {
let userInfo = [NSLocalizedDescriptionKey: "Response is missing data field."]
throw FunctionsErrorCode.internal.generatedError(userInfo: userInfo)
throw FunctionsError(.internal, userInfo: userInfo)
}

return dataJSON
Expand Down
164 changes: 85 additions & 79 deletions FirebaseFunctions/Sources/FunctionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,16 @@ public let FunctionsErrorDetailsKey: String = "details"
case unauthenticated = 16
}

extension FunctionsErrorCode {
private extension FunctionsErrorCode {
/// Takes an HTTP status code and returns the corresponding `FIRFunctionsErrorCode` error code.
///
/// + This is the standard HTTP status code -> error mapping defined in:
/// https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
///
/// - Parameter status: An HTTP status code.
/// - Parameter httpStatusCode: An HTTP status code.
/// - Returns: A `FunctionsErrorCode`. Falls back to `internal` for unknown status codes.
static func errorCode(forHTTPStatus status: Int) -> Self {
switch status {
init(httpStatusCode: Int) {
self = switch httpStatusCode {
case 200: .OK
case 400: .invalidArgument
case 401: .unauthenticated
Expand All @@ -127,102 +127,86 @@ extension FunctionsErrorCode {
}
}

static func errorCode(forName name: String) -> FunctionsErrorCode {
switch name {
case "OK": return .OK
case "CANCELLED": return .cancelled
case "UNKNOWN": return .unknown
case "INVALID_ARGUMENT": return .invalidArgument
case "DEADLINE_EXCEEDED": return .deadlineExceeded
case "NOT_FOUND": return .notFound
case "ALREADY_EXISTS": return .alreadyExists
case "PERMISSION_DENIED": return .permissionDenied
case "RESOURCE_EXHAUSTED": return .resourceExhausted
case "FAILED_PRECONDITION": return .failedPrecondition
case "ABORTED": return .aborted
case "OUT_OF_RANGE": return .outOfRange
case "UNIMPLEMENTED": return .unimplemented
case "INTERNAL": return .internal
case "UNAVAILABLE": return .unavailable
case "DATA_LOSS": return .dataLoss
case "UNAUTHENTICATED": return .unauthenticated
default: return .internal
init(errorName: String) {
self = switch errorName {
case "OK": .OK
case "CANCELLED": .cancelled
case "UNKNOWN": .unknown
case "INVALID_ARGUMENT": .invalidArgument
case "DEADLINE_EXCEEDED": .deadlineExceeded
case "NOT_FOUND": .notFound
case "ALREADY_EXISTS": .alreadyExists
case "PERMISSION_DENIED": .permissionDenied
case "RESOURCE_EXHAUSTED": .resourceExhausted
case "FAILED_PRECONDITION": .failedPrecondition
case "ABORTED": .aborted
case "OUT_OF_RANGE": .outOfRange
case "UNIMPLEMENTED": .unimplemented
case "INTERNAL": .internal
case "UNAVAILABLE": .unavailable
case "DATA_LOSS": .dataLoss
case "UNAUTHENTICATED": .unauthenticated
default: .internal
}
}
}

var descriptionForErrorCode: String {
switch self {
case .OK:
return "OK"
case .cancelled:
return "CANCELLED"
case .unknown:
return "UNKNOWN"
case .invalidArgument:
return "INVALID ARGUMENT"
case .deadlineExceeded:
return "DEADLINE EXCEEDED"
case .notFound:
return "NOT FOUND"
case .alreadyExists:
return "ALREADY EXISTS"
case .permissionDenied:
return "PERMISSION DENIED"
case .resourceExhausted:
return "RESOURCE EXHAUSTED"
case .failedPrecondition:
return "FAILED PRECONDITION"
case .aborted:
return "ABORTED"
case .outOfRange:
return "OUT OF RANGE"
case .unimplemented:
return "UNIMPLEMENTED"
case .internal:
return "INTERNAL"
case .unavailable:
return "UNAVAILABLE"
case .dataLoss:
return "DATA LOSS"
case .unauthenticated:
return "UNAUTHENTICATED"
}
}
/// The object used to report errors that occur during a function’s execution.
struct FunctionsError: CustomNSError {
static let errorDomain = FunctionsErrorDomain

let code: FunctionsErrorCode
let errorUserInfo: [String: Any]
var errorCode: FunctionsErrorCode.RawValue { code.rawValue }

func generatedError(userInfo: [String: Any]? = nil) -> NSError {
return NSError(domain: FunctionsErrorDomain,
code: rawValue,
userInfo: userInfo ?? [NSLocalizedDescriptionKey: descriptionForErrorCode])
init(_ code: FunctionsErrorCode, userInfo: [String: Any]? = nil) {
self.code = code
errorUserInfo = userInfo ?? [NSLocalizedDescriptionKey: Self.errorDescription(from: code)]
}

static func errorForResponse(status: Int,
body: Data?,
serializer: FunctionsSerializer) -> NSError? {
/// Initializes a `FunctionsError` from the HTTP status code and response body.
///
/// - Parameters:
/// - httpStatusCode: The HTTP status code reported during a function’s execution. Only a subset
/// of codes are supported.
/// - body: The optional response data which may contain information about the error. The
/// following schema is expected:
/// ```
/// {
/// "error": {
/// "status": "PERMISSION_DENIED",
/// "message": "You are not allowed to perform this operation",
/// "details": 123 // Any value supported by `FunctionsSerializer`
/// }
/// ```
/// - serializer: The `FunctionsSerializer` used to decode `details` in the error body.
init?(httpStatusCode: Int, body: Data?, serializer: FunctionsSerializer) {
// Start with reasonable defaults from the status code.
var code = FunctionsErrorCode.errorCode(forHTTPStatus: status)
var description = code.descriptionForErrorCode
var details: AnyObject?
var code = FunctionsErrorCode(httpStatusCode: httpStatusCode)
var description = Self.errorDescription(from: code)
var details: Any?

// Then look through the body for explicit details.
if let body,
let json = try? JSONSerialization.jsonObject(with: body) as? NSDictionary,
let errorDetails = json["error"] as? NSDictionary {
let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any],
let errorDetails = json["error"] as? [String: Any] {
if let status = errorDetails["status"] as? String {
code = .errorCode(forName: status)
code = FunctionsErrorCode(errorName: status)

// If the code in the body is invalid, treat the whole response as malformed.
guard code != .internal else {
return code.generatedError(userInfo: nil)
self.init(code)
return
}
}

if let message = errorDetails["message"] as? String {
description = message
} else {
description = code.descriptionForErrorCode
description = Self.errorDescription(from: code)
}

details = errorDetails["details"] as AnyObject?
details = errorDetails["details"] as Any?
// Update `details` only if decoding succeeds;
// otherwise, keep the original object.
if let innerDetails = details,
Expand All @@ -243,6 +227,28 @@ extension FunctionsErrorCode {
if let details {
userInfo[FunctionsErrorDetailsKey] = details
}
return code.generatedError(userInfo: userInfo)
self.init(code, userInfo: userInfo)
}

private static func errorDescription(from code: FunctionsErrorCode) -> String {
switch code {
case .OK: "OK"
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 .internal: "INTERNAL"
case .unavailable: "UNAVAILABLE"
case .dataLoss: "DATA LOSS"
case .unauthenticated: "UNAUTHENTICATED"
}
}
}
Loading
Loading