Coverage Report

Created: 2026-02-11 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/grpc-swift/Sources/GRPC/GRPCStatus.swift
Line
Count
Source
1
/*
2
 * Copyright 2019, gRPC Authors All rights reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
import NIOCore
17
import NIOHTTP1
18
import NIOHTTP2
19
20
/// Encapsulates the result of a gRPC call.
21
public struct GRPCStatus: Error, Sendable {
22
  /// Storage for message/cause. In the happy case ('ok') there will not be a message or cause
23
  /// and this will reference a static storage containing nil values. Making it optional makes the
24
  /// setters for message and cause a little messy.
25
  private var storage: Storage
26
27
  /// The status code of the RPC.
28
  public var code: Code
29
30
  /// The status message of the RPC.
31
  public var message: String? {
32
3.16M
    get {
33
3.16M
      return self.storage.message
34
3.16M
    }
35
0
    set {
36
0
      if isKnownUniquelyReferenced(&self.storage) {
37
0
        self.storage.message = newValue
38
0
      } else {
39
0
        self.storage = .makeStorage(message: newValue, cause: self.storage.cause)
40
0
      }
41
0
    }
42
  }
43
44
  /// The cause of an error (not 'ok') status. This value is never transmitted over the wire and is
45
  /// **not** included in equality checks.
46
  public var cause: Error? {
47
0
    get {
48
0
      return self.storage.cause
49
0
    }
50
0
    set {
51
0
      if isKnownUniquelyReferenced(&self.storage) {
52
0
        self.storage.cause = newValue
53
0
      } else {
54
0
        self.storage = .makeStorage(message: self.storage.message, cause: newValue)
55
0
      }
56
0
    }
57
  }
58
59
  // Backing storage for 'message' and 'cause'.
60
  fileprivate final class Storage {
61
    // On many happy paths there will be no message or cause, so we'll use this shared reference
62
    // instead of allocating a new storage each time.
63
    //
64
    // Alternatively: `GRPCStatus` could hold a storage optionally however doing so made the code
65
    // quite unreadable.
66
    private static let none = Storage(message: nil, cause: nil)
67
68
8.95M
    private init(message: String?, cause: Error?) {
69
8.95M
      self.message = message
70
8.95M
      self.cause = cause
71
8.95M
    }
72
73
    fileprivate var message: Optional<String>
74
    fileprivate var cause: Optional<Error>
75
76
27.3M
    fileprivate static func makeStorage(message: String?, cause: Error?) -> Storage {
77
27.3M
      if message == nil, cause == nil {
78
442
        return Storage.none
79
27.3M
      } else {
80
27.3M
        return Storage(message: message, cause: cause)
81
27.3M
      }
82
27.3M
    }
83
  }
84
85
  /// Whether the status is '.ok'.
86
0
  public var isOk: Bool {
87
0
    return self.code == .ok
88
0
  }
89
90
3.63M
  public init(code: Code, message: String?) {
91
3.63M
    self.init(code: code, message: message, cause: nil)
92
3.63M
  }
93
94
27.3M
  public init(code: Code, message: String? = nil, cause: Error? = nil) {
95
27.3M
    self.code = code
96
27.3M
    self.storage = .makeStorage(message: message, cause: cause)
97
27.3M
  }
98
99
  // Frequently used "default" statuses.
100
101
  /// The default status to return for succeeded calls.
102
  ///
103
  /// - Important: This should *not* be used when checking whether a returned status has an 'ok'
104
  ///   status code. Use `GRPCStatus.isOk` or check the code directly.
105
  public static let ok = GRPCStatus(code: .ok, message: nil)
106
  /// "Internal server error" status.
107
  public static let processingError = Self.processingError(cause: nil)
108
109
152k
  public static func processingError(cause: Error?) -> GRPCStatus {
110
152k
    return GRPCStatus(
111
152k
      code: .internalError,
112
152k
      message: "unknown error processing request",
113
152k
      cause: cause
114
152k
    )
115
152k
  }
116
}
117
118
extension GRPCStatus: Equatable {
119
1.86M
  public static func == (lhs: GRPCStatus, rhs: GRPCStatus) -> Bool {
120
1.86M
    return lhs.code == rhs.code && lhs.message == rhs.message
121
1.86M
  }
122
}
123
124
extension GRPCStatus: CustomStringConvertible {
125
0
  public var description: String {
126
0
    switch (self.message, self.cause) {
127
0
    case let (.some(message), .some(cause)):
128
0
      return "\(self.code): \(message), cause: \(cause)"
129
0
    case let (.some(message), .none):
130
0
      return "\(self.code): \(message)"
131
0
    case let (.none, .some(cause)):
132
0
      return "\(self.code), cause: \(cause)"
133
0
    case (.none, .none):
134
0
      return "\(self.code)"
135
0
    }
136
0
  }
137
}
138
139
extension GRPCStatus {
140
0
  internal var testingOnly_storageObjectIdentifier: ObjectIdentifier {
141
0
    return ObjectIdentifier(self.storage)
142
0
  }
143
}
144
145
extension GRPCStatus {
146
  /// Status codes for gRPC operations (replicated from `status_code_enum.h` in the
147
  /// [gRPC core library](https://github.com/grpc/grpc)).
148
  public struct Code: Hashable, CustomStringConvertible, Sendable {
149
    // `rawValue` must be an `Int` for API reasons and we don't need (or want) to store anything so
150
    // wide, a `UInt8` is fine.
151
    private let _rawValue: UInt8
152
153
281k
    public var rawValue: Int {
154
281k
      return Int(self._rawValue)
155
281k
    }
156
157
0
    public init?(rawValue: Int) {
158
0
      switch rawValue {
159
0
      case 0 ... 16:
160
0
        self._rawValue = UInt8(truncatingIfNeeded: rawValue)
161
0
      default:
162
0
        return nil
163
0
      }
164
0
    }
165
166
10
    private init(_ code: UInt8) {
167
10
      self._rawValue = code
168
10
    }
169
170
    /// Not an error; returned on success.
171
    public static let ok = Code(0)
172
173
    /// The operation was cancelled (typically by the caller).
174
    public static let cancelled = Code(1)
175
176
    /// Unknown error. An example of where this error may be returned is if a
177
    /// Status value received from another address space belongs to an error-space
178
    /// that is not known in this address space. Also errors raised by APIs that
179
    /// do not return enough error information may be converted to this error.
180
    public static let unknown = Code(2)
181
182
    /// Client specified an invalid argument. Note that this differs from
183
    /// FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are
184
    /// problematic regardless of the state of the system (e.g., a malformed file
185
    /// name).
186
    public static let invalidArgument = Code(3)
187
188
    /// Deadline expired before operation could complete. For operations that
189
    /// change the state of the system, this error may be returned even if the
190
    /// operation has completed successfully. For example, a successful response
191
    /// from a server could have been delayed long enough for the deadline to
192
    /// expire.
193
    public static let deadlineExceeded = Code(4)
194
195
    /// Some requested entity (e.g., file or directory) was not found.
196
    public static let notFound = Code(5)
197
198
    /// Some entity that we attempted to create (e.g., file or directory) already
199
    /// exists.
200
    public static let alreadyExists = Code(6)
201
202
    /// The caller does not have permission to execute the specified operation.
203
    /// PERMISSION_DENIED must not be used for rejections caused by exhausting
204
    /// some resource (use RESOURCE_EXHAUSTED instead for those errors).
205
    /// PERMISSION_DENIED must not be used if the caller can not be identified
206
    /// (use UNAUTHENTICATED instead for those errors).
207
    public static let permissionDenied = Code(7)
208
209
    /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the
210
    /// entire file system is out of space.
211
    public static let resourceExhausted = Code(8)
212
213
    /// Operation was rejected because the system is not in a state required for
214
    /// the operation's execution. For example, directory to be deleted may be
215
    /// non-empty, an rmdir operation is applied to a non-directory, etc.
216
    ///
217
    /// A litmus test that may help a service implementor in deciding
218
    /// between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE:
219
    ///  (a) Use UNAVAILABLE if the client can retry just the failing call.
220
    ///  (b) Use ABORTED if the client should retry at a higher-level
221
    ///      (e.g., restarting a read-modify-write sequence).
222
    ///  (c) Use FAILED_PRECONDITION if the client should not retry until
223
    ///      the system state has been explicitly fixed. E.g., if an "rmdir"
224
    ///      fails because the directory is non-empty, FAILED_PRECONDITION
225
    ///      should be returned since the client should not retry unless
226
    ///      they have first fixed up the directory by deleting files from it.
227
    ///  (d) Use FAILED_PRECONDITION if the client performs conditional
228
    ///      REST Get/Update/Delete on a resource and the resource on the
229
    ///      server does not match the condition. E.g., conflicting
230
    ///      read-modify-write on the same resource.
231
    public static let failedPrecondition = Code(9)
232
233
    /// The operation was aborted, typically due to a concurrency issue like
234
    /// sequencer check failures, transaction aborts, etc.
235
    ///
236
    /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED,
237
    /// and UNAVAILABLE.
238
    public static let aborted = Code(10)
239
240
    /// Operation was attempted past the valid range. E.g., seeking or reading
241
    /// past end of file.
242
    ///
243
    /// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed
244
    /// if the system state changes. For example, a 32-bit file system will
245
    /// generate INVALID_ARGUMENT if asked to read at an offset that is not in the
246
    /// range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from
247
    /// an offset past the current file size.
248
    ///
249
    /// There is a fair bit of overlap between FAILED_PRECONDITION and
250
    /// OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error)
251
    /// when it applies so that callers who are iterating through a space can
252
    /// easily look for an OUT_OF_RANGE error to detect when they are done.
253
    public static let outOfRange = Code(11)
254
255
    /// Operation is not implemented or not supported/enabled in this service.
256
    public static let unimplemented = Code(12)
257
258
    /// Internal errors. Means some invariants expected by underlying System has
259
    /// been broken. If you see one of these errors, Something is very broken.
260
    public static let internalError = Code(13)
261
262
    /// The service is currently unavailable. This is a most likely a transient
263
    /// condition and may be corrected by retrying with a backoff.
264
    ///
265
    /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED,
266
    /// and UNAVAILABLE.
267
    public static let unavailable = Code(14)
268
269
    /// Unrecoverable data loss or corruption.
270
    public static let dataLoss = Code(15)
271
272
    /// The request does not have valid authentication credentials for the
273
    /// operation.
274
    public static let unauthenticated = Code(16)
275
276
0
    public var description: String {
277
0
      switch self {
278
0
      case .ok:
279
0
        return "ok (\(self._rawValue))"
280
0
      case .cancelled:
281
0
        return "cancelled (\(self._rawValue))"
282
0
      case .unknown:
283
0
        return "unknown (\(self._rawValue))"
284
0
      case .invalidArgument:
285
0
        return "invalid argument (\(self._rawValue))"
286
0
      case .deadlineExceeded:
287
0
        return "deadline exceeded (\(self._rawValue))"
288
0
      case .notFound:
289
0
        return "not found (\(self._rawValue))"
290
0
      case .alreadyExists:
291
0
        return "already exists (\(self._rawValue))"
292
0
      case .permissionDenied:
293
0
        return "permission denied (\(self._rawValue))"
294
0
      case .resourceExhausted:
295
0
        return "resource exhausted (\(self._rawValue))"
296
0
      case .failedPrecondition:
297
0
        return "failed precondition (\(self._rawValue))"
298
0
      case .aborted:
299
0
        return "aborted (\(self._rawValue))"
300
0
      case .outOfRange:
301
0
        return "out of range (\(self._rawValue))"
302
0
      case .unimplemented:
303
0
        return "unimplemented (\(self._rawValue))"
304
0
      case .internalError:
305
0
        return "internal error (\(self._rawValue))"
306
0
      case .unavailable:
307
0
        return "unavailable (\(self._rawValue))"
308
0
      case .dataLoss:
309
0
        return "data loss (\(self._rawValue))"
310
0
      case .unauthenticated:
311
0
        return "unauthenticated (\(self._rawValue))"
312
0
      default:
313
0
        return String(describing: self._rawValue)
314
0
      }
315
0
    }
316
  }
317
}
318
319
// `GRPCStatus` has CoW semantics so it is inherently `Sendable`. Rather than marking `GRPCStatus`
320
// as `@unchecked Sendable` we only mark `Storage` as such.
321
extension GRPCStatus.Storage: @unchecked Sendable {}
322
323
/// This protocol serves as a customisation point for error types so that gRPC calls may be
324
/// terminated with an appropriate status.
325
public protocol GRPCStatusTransformable: Error {
326
  /// Make a `GRPCStatus` from the underlying error.
327
  ///
328
  /// - Returns: A `GRPCStatus` representing the underlying error.
329
  func makeGRPCStatus() -> GRPCStatus
330
}
331
332
extension GRPCStatus: GRPCStatusTransformable {
333
0
  public func makeGRPCStatus() -> GRPCStatus {
334
0
    return self
335
0
  }
336
}
337
338
extension NIOHTTP2Errors.StreamClosed: GRPCStatusTransformable {
339
0
  public func makeGRPCStatus() -> GRPCStatus {
340
0
    return .init(code: .unavailable, message: self.localizedDescription, cause: self)
341
0
  }
342
}
343
344
extension NIOHTTP2Errors.IOOnClosedConnection: GRPCStatusTransformable {
345
0
  public func makeGRPCStatus() -> GRPCStatus {
346
0
    return .init(code: .unavailable, message: "The connection is closed", cause: self)
347
0
  }
348
}
349
350
extension ChannelError: GRPCStatusTransformable {
351
0
  public func makeGRPCStatus() -> GRPCStatus {
352
0
    switch self {
353
0
    case .inputClosed, .outputClosed, .ioOnClosedChannel:
354
0
      return .init(code: .unavailable, message: "The connection is closed", cause: self)
355
0
356
0
    default:
357
0
      var processingError = GRPCStatus.processingError
358
0
      processingError.cause = self
359
0
      return processingError
360
0
    }
361
0
  }
362
}