Skip to content

Commit

Permalink
FunctionsError (#13601)
Browse files Browse the repository at this point in the history
  • Loading branch information
yakovmanshin authored Sep 10, 2024
1 parent f18a459 commit 9384b9f
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 90 deletions.
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

0 comments on commit 9384b9f

Please sign in to comment.