Skip to content

Commit 9384b9f

Browse files
authored
FunctionsError (#13601)
1 parent f18a459 commit 9384b9f

File tree

3 files changed

+264
-90
lines changed

3 files changed

+264
-90
lines changed

FirebaseFunctions/Sources/Functions.swift

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -509,13 +509,13 @@ enum FunctionsConstants {
509509
if let error = error as NSError? {
510510
let localError: (any Error)?
511511
if error.domain == kGTMSessionFetcherStatusDomain {
512-
localError = FunctionsErrorCode.errorForResponse(
513-
status: error.code,
512+
localError = FunctionsError(
513+
httpStatusCode: error.code,
514514
body: data,
515515
serializer: serializer
516516
)
517517
} else if error.domain == NSURLErrorDomain, error.code == NSURLErrorTimedOut {
518-
localError = FunctionsErrorCode.deadlineExceeded.generatedError(userInfo: nil)
518+
localError = FunctionsError(.deadlineExceeded)
519519
} else {
520520
localError = nil
521521
}
@@ -525,15 +525,11 @@ enum FunctionsConstants {
525525

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

531531
// Case 3: `data` is not `nil` but might specify a custom error -> throws conditionally
532-
if let bodyError = FunctionsErrorCode.errorForResponse(
533-
status: 200,
534-
body: data,
535-
serializer: serializer
536-
) {
532+
if let bodyError = FunctionsError(httpStatusCode: 200, body: data, serializer: serializer) {
537533
throw bodyError
538534
}
539535

@@ -546,13 +542,13 @@ enum FunctionsConstants {
546542

547543
guard let responseJSON = responseJSONObject as? NSDictionary else {
548544
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
549-
throw FunctionsErrorCode.internal.generatedError(userInfo: userInfo)
545+
throw FunctionsError(.internal, userInfo: userInfo)
550546
}
551547

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

558554
return dataJSON

FirebaseFunctions/Sources/FunctionsError.swift

Lines changed: 85 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,16 @@ public let FunctionsErrorDetailsKey: String = "details"
101101
case unauthenticated = 16
102102
}
103103

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

130-
static func errorCode(forName name: String) -> FunctionsErrorCode {
131-
switch name {
132-
case "OK": return .OK
133-
case "CANCELLED": return .cancelled
134-
case "UNKNOWN": return .unknown
135-
case "INVALID_ARGUMENT": return .invalidArgument
136-
case "DEADLINE_EXCEEDED": return .deadlineExceeded
137-
case "NOT_FOUND": return .notFound
138-
case "ALREADY_EXISTS": return .alreadyExists
139-
case "PERMISSION_DENIED": return .permissionDenied
140-
case "RESOURCE_EXHAUSTED": return .resourceExhausted
141-
case "FAILED_PRECONDITION": return .failedPrecondition
142-
case "ABORTED": return .aborted
143-
case "OUT_OF_RANGE": return .outOfRange
144-
case "UNIMPLEMENTED": return .unimplemented
145-
case "INTERNAL": return .internal
146-
case "UNAVAILABLE": return .unavailable
147-
case "DATA_LOSS": return .dataLoss
148-
case "UNAUTHENTICATED": return .unauthenticated
149-
default: return .internal
130+
init(errorName: String) {
131+
self = switch errorName {
132+
case "OK": .OK
133+
case "CANCELLED": .cancelled
134+
case "UNKNOWN": .unknown
135+
case "INVALID_ARGUMENT": .invalidArgument
136+
case "DEADLINE_EXCEEDED": .deadlineExceeded
137+
case "NOT_FOUND": .notFound
138+
case "ALREADY_EXISTS": .alreadyExists
139+
case "PERMISSION_DENIED": .permissionDenied
140+
case "RESOURCE_EXHAUSTED": .resourceExhausted
141+
case "FAILED_PRECONDITION": .failedPrecondition
142+
case "ABORTED": .aborted
143+
case "OUT_OF_RANGE": .outOfRange
144+
case "UNIMPLEMENTED": .unimplemented
145+
case "INTERNAL": .internal
146+
case "UNAVAILABLE": .unavailable
147+
case "DATA_LOSS": .dataLoss
148+
case "UNAUTHENTICATED": .unauthenticated
149+
default: .internal
150150
}
151151
}
152+
}
152153

153-
var descriptionForErrorCode: String {
154-
switch self {
155-
case .OK:
156-
return "OK"
157-
case .cancelled:
158-
return "CANCELLED"
159-
case .unknown:
160-
return "UNKNOWN"
161-
case .invalidArgument:
162-
return "INVALID ARGUMENT"
163-
case .deadlineExceeded:
164-
return "DEADLINE EXCEEDED"
165-
case .notFound:
166-
return "NOT FOUND"
167-
case .alreadyExists:
168-
return "ALREADY EXISTS"
169-
case .permissionDenied:
170-
return "PERMISSION DENIED"
171-
case .resourceExhausted:
172-
return "RESOURCE EXHAUSTED"
173-
case .failedPrecondition:
174-
return "FAILED PRECONDITION"
175-
case .aborted:
176-
return "ABORTED"
177-
case .outOfRange:
178-
return "OUT OF RANGE"
179-
case .unimplemented:
180-
return "UNIMPLEMENTED"
181-
case .internal:
182-
return "INTERNAL"
183-
case .unavailable:
184-
return "UNAVAILABLE"
185-
case .dataLoss:
186-
return "DATA LOSS"
187-
case .unauthenticated:
188-
return "UNAUTHENTICATED"
189-
}
190-
}
154+
/// The object used to report errors that occur during a function’s execution.
155+
struct FunctionsError: CustomNSError {
156+
static let errorDomain = FunctionsErrorDomain
157+
158+
let code: FunctionsErrorCode
159+
let errorUserInfo: [String: Any]
160+
var errorCode: FunctionsErrorCode.RawValue { code.rawValue }
191161

192-
func generatedError(userInfo: [String: Any]? = nil) -> NSError {
193-
return NSError(domain: FunctionsErrorDomain,
194-
code: rawValue,
195-
userInfo: userInfo ?? [NSLocalizedDescriptionKey: descriptionForErrorCode])
162+
init(_ code: FunctionsErrorCode, userInfo: [String: Any]? = nil) {
163+
self.code = code
164+
errorUserInfo = userInfo ?? [NSLocalizedDescriptionKey: Self.errorDescription(from: code)]
196165
}
197166

198-
static func errorForResponse(status: Int,
199-
body: Data?,
200-
serializer: FunctionsSerializer) -> NSError? {
167+
/// Initializes a `FunctionsError` from the HTTP status code and response body.
168+
///
169+
/// - Parameters:
170+
/// - httpStatusCode: The HTTP status code reported during a function’s execution. Only a subset
171+
/// of codes are supported.
172+
/// - body: The optional response data which may contain information about the error. The
173+
/// following schema is expected:
174+
/// ```
175+
/// {
176+
/// "error": {
177+
/// "status": "PERMISSION_DENIED",
178+
/// "message": "You are not allowed to perform this operation",
179+
/// "details": 123 // Any value supported by `FunctionsSerializer`
180+
/// }
181+
/// ```
182+
/// - serializer: The `FunctionsSerializer` used to decode `details` in the error body.
183+
init?(httpStatusCode: Int, body: Data?, serializer: FunctionsSerializer) {
201184
// Start with reasonable defaults from the status code.
202-
var code = FunctionsErrorCode.errorCode(forHTTPStatus: status)
203-
var description = code.descriptionForErrorCode
204-
var details: AnyObject?
185+
var code = FunctionsErrorCode(httpStatusCode: httpStatusCode)
186+
var description = Self.errorDescription(from: code)
187+
var details: Any?
205188

206189
// Then look through the body for explicit details.
207190
if let body,
208-
let json = try? JSONSerialization.jsonObject(with: body) as? NSDictionary,
209-
let errorDetails = json["error"] as? NSDictionary {
191+
let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any],
192+
let errorDetails = json["error"] as? [String: Any] {
210193
if let status = errorDetails["status"] as? String {
211-
code = .errorCode(forName: status)
194+
code = FunctionsErrorCode(errorName: status)
212195

213196
// If the code in the body is invalid, treat the whole response as malformed.
214197
guard code != .internal else {
215-
return code.generatedError(userInfo: nil)
198+
self.init(code)
199+
return
216200
}
217201
}
218202

219203
if let message = errorDetails["message"] as? String {
220204
description = message
221205
} else {
222-
description = code.descriptionForErrorCode
206+
description = Self.errorDescription(from: code)
223207
}
224208

225-
details = errorDetails["details"] as AnyObject?
209+
details = errorDetails["details"] as Any?
226210
// Update `details` only if decoding succeeds;
227211
// otherwise, keep the original object.
228212
if let innerDetails = details,
@@ -243,6 +227,28 @@ extension FunctionsErrorCode {
243227
if let details {
244228
userInfo[FunctionsErrorDetailsKey] = details
245229
}
246-
return code.generatedError(userInfo: userInfo)
230+
self.init(code, userInfo: userInfo)
231+
}
232+
233+
private static func errorDescription(from code: FunctionsErrorCode) -> String {
234+
switch code {
235+
case .OK: "OK"
236+
case .cancelled: "CANCELLED"
237+
case .unknown: "UNKNOWN"
238+
case .invalidArgument: "INVALID ARGUMENT"
239+
case .deadlineExceeded: "DEADLINE EXCEEDED"
240+
case .notFound: "NOT FOUND"
241+
case .alreadyExists: "ALREADY EXISTS"
242+
case .permissionDenied: "PERMISSION DENIED"
243+
case .resourceExhausted: "RESOURCE EXHAUSTED"
244+
case .failedPrecondition: "FAILED PRECONDITION"
245+
case .aborted: "ABORTED"
246+
case .outOfRange: "OUT OF RANGE"
247+
case .unimplemented: "UNIMPLEMENTED"
248+
case .internal: "INTERNAL"
249+
case .unavailable: "UNAVAILABLE"
250+
case .dataLoss: "DATA LOSS"
251+
case .unauthenticated: "UNAUTHENTICATED"
252+
}
247253
}
248254
}

0 commit comments

Comments
 (0)