-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathFunctionsError.swift
257 lines (224 loc) · 8.72 KB
/
FunctionsError.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// Copyright 2022 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
/// The error domain for codes in the ``FunctionsErrorCode`` enum.
public let FunctionsErrorDomain: String = "com.firebase.functions"
/// The key for finding error details in the `NSError` userInfo.
public let FunctionsErrorDetailsKey: String = "details"
/**
* The set of error status codes that can be returned from a Callable HTTPS trigger. These are the
* canonical error codes for Google APIs, as documented here:
* https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto#L26
*/
@objc(FIRFunctionsErrorCode) public enum FunctionsErrorCode: Int, Sendable {
/** The operation completed successfully. */
case OK = 0
/** The operation was cancelled (typically by the caller). */
case cancelled = 1
/** Unknown error or an error from a different error domain. */
case unknown = 2
/**
* Client specified an invalid argument. Note that this differs from `FailedPrecondition`.
* `InvalidArgument` indicates arguments that are problematic regardless of the state of the
* system (e.g., an invalid field name).
*/
case invalidArgument = 3
/**
* Deadline expired before operation could complete. For operations that change the state of the
* system, this error may be returned even if the operation has completed successfully. For
* example, a successful response from a server could have been delayed long enough for the
* deadline to expire.
*/
case deadlineExceeded = 4
/** Some requested document was not found. */
case notFound = 5
/** Some document that we attempted to create already exists. */
case alreadyExists = 6
/** The caller does not have permission to execute the specified operation. */
case permissionDenied = 7
/**
* Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
* is out of space.
*/
case resourceExhausted = 8
/**
* Operation was rejected because the system is not in a state required for the operation's
* execution.
*/
case failedPrecondition = 9
/**
* The operation was aborted, typically due to a concurrency issue like transaction aborts, etc.
*/
case aborted = 10
/** Operation was attempted past the valid range. */
case outOfRange = 11
/** Operation is not implemented or not supported/enabled. */
case unimplemented = 12
/**
* Internal errors. Means some invariant expected by underlying system has been broken. If you
* see one of these errors, something is very broken.
*/
case `internal` = 13
/**
* The service is currently unavailable. This is a most likely a transient condition and may be
* corrected by retrying with a backoff.
*/
case unavailable = 14
/** Unrecoverable data loss or corruption. */
case dataLoss = 15
/** The request does not have valid authentication credentials for the operation. */
case unauthenticated = 16
}
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 httpStatusCode: An HTTP status code.
/// - Returns: A `FunctionsErrorCode`. Falls back to `internal` for unknown status codes.
init(httpStatusCode: Int) {
self = switch httpStatusCode {
case 200: .OK
case 400: .invalidArgument
case 401: .unauthenticated
case 403: .permissionDenied
case 404: .notFound
case 409: .alreadyExists
case 429: .resourceExhausted
case 499: .cancelled
case 500: .internal
case 501: .unimplemented
case 503: .unavailable
case 504: .deadlineExceeded
default: .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
}
}
}
/// 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 }
init(_ code: FunctionsErrorCode, userInfo: [String: Any]? = nil) {
self.code = code
errorUserInfo = userInfo ?? [NSLocalizedDescriptionKey: Self.errorDescription(from: code)]
}
/// 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, region: String, url: URL, body: Data?,
serializer: FunctionsSerializer) {
// Start with reasonable defaults from the status code.
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? [String: Any],
let errorDetails = json["error"] as? [String: Any] {
if let status = errorDetails["status"] as? String {
code = FunctionsErrorCode(errorName: status)
// If the code in the body is invalid, treat the whole response as malformed.
guard code != .internal else {
self.init(code)
return
}
}
if let message = errorDetails["message"] as? String {
description = message
} else {
description = Self.errorDescription(from: code)
}
details = errorDetails["details"] as Any?
// Update `details` only if decoding succeeds;
// otherwise, keep the original object.
if let innerDetails = details,
let decodedDetails = try? serializer.decode(innerDetails) {
details = decodedDetails
}
}
if code == .OK {
// Technically, there's an edge case where a developer could explicitly return an error code
// of
// OK, and we will treat it as success, but that seems reasonable.
return nil
}
var userInfo = [String: Any]()
userInfo[NSLocalizedDescriptionKey] = description
userInfo["region"] = region
userInfo["url"] = url
if let details {
userInfo[FunctionsErrorDetailsKey] = details
}
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"
}
}
}