Skip to content

Commit 53ecb41

Browse files
committed
Add RPCErrorConvertible
Motivation: If an error is thrown from a server RPC then the status sent to the client will always have the unknown error code unless an `RPCError` is thrown. Moreover, there are various extensions to gRPC which rely on additional information being stuffed into the metadata. This is difficult and a bit error prone for users to do directly. We should provide a mechanism whereby errors can be converted to an `RPCError` such that the appropriate code, message and metadata are sent to the client. Modifications: - Add the `RPCErrorConvertible` protocol. Conforming types provide appropriate properties to populate an `RPCError`. - Add handling for this in the server executor such that convertible errors are converted into an `RPCError`. Result: Easier for users to propagate an appropriate status
1 parent e390c85 commit 53ecb41

File tree

4 files changed

+134
-1
lines changed

4 files changed

+134
-1
lines changed

Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,15 @@ struct ServerRPCExecutor {
188188
try await handler(request, context)
189189
}
190190
}.castError(to: RPCError.self) { error in
191-
RPCError(code: .unknown, message: "Service method threw an unknown error.", cause: error)
191+
if let convertible = error as? (any RPCErrorConvertible) {
192+
return RPCError(convertible)
193+
} else {
194+
return RPCError(
195+
code: .unknown,
196+
message: "Service method threw an unknown error.",
197+
cause: error
198+
)
199+
}
192200
}.flatMap { response in
193201
response.accepted
194202
}

Sources/GRPCCore/RPCError.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,60 @@ extension RPCError.Code {
277277
/// operation.
278278
public static let unauthenticated = Self(code: .unauthenticated)
279279
}
280+
281+
/// A value that can be converted to an ``RPCError``.
282+
///
283+
/// You can conform types to this protocol to have more control over the status codes and
284+
/// error information provided to clients when a service throws an error.
285+
public protocol RPCErrorConvertible {
286+
/// The error code to terminate the RPC with.
287+
var rpcErrorCode: RPCError.Code { get }
288+
289+
/// A message providing additional context about the error.
290+
var rpcErrorMessage: String { get }
291+
292+
/// Metadata associated with the error.
293+
///
294+
/// Any metadata included in the error thrown from a service will be sent back to the client and
295+
/// conversely any ``RPCError`` received by the client may include metadata sent by a service.
296+
///
297+
/// Note that clients and servers may synthesise errors which may not include metadata.
298+
var rpcErrorMetadata: Metadata { get }
299+
300+
/// The original error which led to this error being thrown.
301+
var rpcErrorCause: (any Error)? { get }
302+
}
303+
304+
extension RPCErrorConvertible {
305+
/// Metadata associated with the error.
306+
///
307+
/// Any metadata included in the error thrown from a service will be sent back to the client and
308+
/// conversely any ``RPCError`` received by the client may include metadata sent by a service.
309+
///
310+
/// Note that clients and servers may synthesise errors which may not include metadata.
311+
public var rpcErrorMetadata: Metadata {
312+
[:]
313+
}
314+
315+
/// The original error which led to this error being thrown.
316+
public var rpcErrorCause: (any Error)? {
317+
nil
318+
}
319+
}
320+
321+
extension RPCErrorConvertible where Self: Error {
322+
/// The original error which led to this error being thrown.
323+
public var rpcErrorCause: (any Error)? {
324+
self
325+
}
326+
}
327+
328+
extension RPCError {
329+
/// Create a new error by converting the given value.
330+
public init(_ convertible: some RPCErrorConvertible) {
331+
self.code = convertible.rpcErrorCode
332+
self.message = convertible.rpcErrorMessage
333+
self.metadata = convertible.rpcErrorMetadata
334+
self.cause = convertible.rpcErrorCause
335+
}
336+
}

Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,27 @@ final class ServerRPCExecutorTests: XCTestCase {
346346
XCTAssertEqual(parts, [.status(Status(code: .unavailable, message: "Unavailable"), [:])])
347347
}
348348
}
349+
350+
func testErrorConversion() async throws {
351+
struct CustomError: RPCErrorConvertible, Error {
352+
var rpcErrorCode: RPCError.Code { .alreadyExists }
353+
var rpcErrorMessage: String { "foobar" }
354+
var rpcErrorMetadata: Metadata { ["error": "yes"] }
355+
}
356+
357+
let harness = ServerRPCExecutorTestHarness()
358+
try await harness.execute(handler: .throwing(CustomError())) { inbound in
359+
try await inbound.write(.metadata(["foo": "bar"]))
360+
try await inbound.write(.message([0]))
361+
await inbound.finish()
362+
} consumer: { outbound in
363+
let parts = try await outbound.collect()
364+
XCTAssertEqual(
365+
parts,
366+
[
367+
.status(Status(code: .alreadyExists, message: "foobar"), ["error": "yes"])
368+
]
369+
)
370+
}
371+
}
349372
}

Tests/GRPCCoreTests/RPCErrorTests.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,49 @@ struct RPCErrorTests {
189189
#expect(wrappedError1.message == "Error 1.")
190190
#expect(wrappedError1.cause == nil)
191191
}
192+
193+
@Test("Convert type to RPCError")
194+
func convertTypeUsingRPCErrorConvertible() {
195+
struct Cause: Error {}
196+
struct ConvertibleError: RPCErrorConvertible {
197+
var rpcErrorCode: RPCError.Code { .unknown }
198+
var rpcErrorMessage: String { "uhoh" }
199+
var rpcErrorMetadata: Metadata { ["k": "v"] }
200+
var rpcErrorCause: (any Error)? { Cause() }
201+
}
202+
203+
let error = RPCError(ConvertibleError())
204+
#expect(error.code == .unknown)
205+
#expect(error.message == "uhoh")
206+
#expect(error.metadata == ["k": "v"])
207+
#expect(error.cause is Cause)
208+
}
209+
210+
@Test("Convert type to RPCError with defaults")
211+
func convertTypeUsingRPCErrorConvertibleDefaults() {
212+
struct ConvertibleType: RPCErrorConvertible {
213+
var rpcErrorCode: RPCError.Code { .unknown }
214+
var rpcErrorMessage: String { "uhoh" }
215+
}
216+
217+
let error = RPCError(ConvertibleType())
218+
#expect(error.code == .unknown)
219+
#expect(error.message == "uhoh")
220+
#expect(error.metadata == [:])
221+
#expect(error.cause == nil)
222+
}
223+
224+
@Test("Convert error to RPCError with defaults")
225+
func convertErrorUsingRPCErrorConvertibleDefaults() {
226+
struct ConvertibleType: RPCErrorConvertible, Error {
227+
var rpcErrorCode: RPCError.Code { .unknown }
228+
var rpcErrorMessage: String { "uhoh" }
229+
}
230+
231+
let error = RPCError(ConvertibleType())
232+
#expect(error.code == .unknown)
233+
#expect(error.message == "uhoh")
234+
#expect(error.metadata == [:])
235+
#expect(error.cause is ConvertibleType)
236+
}
192237
}

0 commit comments

Comments
 (0)