Skip to content

Convert errors thrown from interceptors #2209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 18, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift
Original file line number Diff line number Diff line change
@@ -186,6 +186,8 @@ extension ClientRPCExecutor {
}
} catch let error as RPCError {
return StreamingClientResponse(error: error)
} catch let error as RPCErrorConvertible {
return StreamingClientResponse(error: RPCError(error))
} catch let other {
let error = RPCError(code: .unknown, message: "", cause: other)
return StreamingClientResponse(error: error)
2 changes: 2 additions & 0 deletions Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift
Original file line number Diff line number Diff line change
@@ -330,6 +330,8 @@ extension ServerRPCExecutor {
}
} catch let error as RPCError {
return StreamingServerResponse(error: error)
} catch let error as RPCErrorConvertible {
return StreamingServerResponse(error: RPCError(error))
} catch let other {
let error = RPCError(code: .unknown, message: "", cause: other)
return StreamingServerResponse(error: error)
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ struct ClientRPCExecutorTestHarness {
private let server: ServerStreamHandler
private let clientTransport: StreamCountingClientTransport
private let serverTransport: StreamCountingServerTransport
private let interceptors: [any ClientInterceptor]

var clientStreamsOpened: Int {
self.clientTransport.streamsOpened
@@ -42,8 +43,13 @@ struct ClientRPCExecutorTestHarness {
self.serverTransport.acceptedStreamsCount
}

init(transport: Transport = .inProcess, server: ServerStreamHandler) {
init(
transport: Transport = .inProcess,
server: ServerStreamHandler,
interceptors: [any ClientInterceptor] = []
) {
self.server = server
self.interceptors = interceptors

switch transport {
case .inProcess:
@@ -141,7 +147,7 @@ struct ClientRPCExecutorTestHarness {
serializer: serializer,
deserializer: deserializer,
transport: self.clientTransport,
interceptors: [],
interceptors: self.interceptors,
handler: handler
)

Original file line number Diff line number Diff line change
@@ -268,4 +268,25 @@ final class ClientRPCExecutorTests: XCTestCase {
}
}
}

func testInterceptorErrorConversion() async throws {
struct CustomError: RPCErrorConvertible, Error {
var rpcErrorCode: RPCError.Code { .alreadyExists }
var rpcErrorMessage: String { "foobar" }
var rpcErrorMetadata: Metadata { ["error": "yes"] }
}

let tester = ClientRPCExecutorTestHarness(
server: .echo,
interceptors: [.throwError(CustomError())]
)

try await tester.unary(request: ClientRequest(message: [])) { response in
XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in
XCTAssertEqual(error.code, .alreadyExists)
XCTAssertEqual(error.message, "foobar")
XCTAssertEqual(error.metadata, ["error": "yes"])
}
}
}
}
Original file line number Diff line number Diff line change
@@ -374,4 +374,23 @@ final class ServerRPCExecutorTests: XCTestCase {
)
}
}

func testInterceptorErrorConversion() async throws {
struct CustomError: RPCErrorConvertible, Error {
var rpcErrorCode: RPCError.Code { .alreadyExists }
var rpcErrorMessage: String { "foobar" }
var rpcErrorMetadata: Metadata { ["error": "yes"] }
}

let harness = ServerRPCExecutorTestHarness(interceptors: [.throwError(CustomError())])
try await harness.execute(handler: .throwing(CustomError())) { inbound in
try await inbound.write(.metadata(["foo": "bar"]))
await inbound.finish()
} consumer: { outbound in
let parts = try await outbound.collect()
let status = Status(code: .alreadyExists, message: "foobar")
let metadata: Metadata = ["error": "yes"]
XCTAssertEqual(parts, [.status(status, metadata)])
}
}
}
Original file line number Diff line number Diff line change
@@ -18,11 +18,11 @@ import GRPCCore

extension ClientInterceptor where Self == RejectAllClientInterceptor {
static func rejectAll(with error: RPCError) -> Self {
return RejectAllClientInterceptor(error: error, throw: false)
return RejectAllClientInterceptor(reject: error)
}

static func throwError(_ error: RPCError) -> Self {
return RejectAllClientInterceptor(error: error, throw: true)
static func throwError(_ error: any Error) -> Self {
return RejectAllClientInterceptor(throw: error)
}

}
@@ -35,15 +35,21 @@ extension ClientInterceptor where Self == RequestCountingClientInterceptor {

/// Rejects all RPCs with the provided error.
struct RejectAllClientInterceptor: ClientInterceptor {
/// The error to reject all RPCs with.
let error: RPCError
/// Whether the error should be thrown. If `false` then the request is rejected with the error
/// instead.
let `throw`: Bool
enum Mode: Sendable {
/// Throw the error rather.
case `throw`(any Error)
/// Reject the RPC with a given error.
case reject(RPCError)
}

let mode: Mode

init(throw error: any Error) {
self.mode = .throw(error)
}

init(error: RPCError, throw: Bool = false) {
self.error = error
self.`throw` = `throw`
init(reject error: RPCError) {
self.mode = .reject(error)
}

func intercept<Input: Sendable, Output: Sendable>(
@@ -54,10 +60,11 @@ struct RejectAllClientInterceptor: ClientInterceptor {
ClientContext
) async throws -> StreamingClientResponse<Output>
) async throws -> StreamingClientResponse<Output> {
if self.throw {
throw self.error
} else {
return StreamingClientResponse(error: self.error)
switch self.mode {
case .throw(let error):
throw error
case .reject(let error):
return StreamingClientResponse(error: error)
}
}
}
Original file line number Diff line number Diff line change
@@ -18,11 +18,11 @@ import GRPCCore

extension ServerInterceptor where Self == RejectAllServerInterceptor {
static func rejectAll(with error: RPCError) -> Self {
return RejectAllServerInterceptor(error: error, throw: false)
return RejectAllServerInterceptor(reject: error)
}

static func throwError(_ error: RPCError) -> Self {
RejectAllServerInterceptor(error: error, throw: true)
static func throwError(_ error: any Error) -> Self {
RejectAllServerInterceptor(throw: error)
}
}

@@ -34,15 +34,21 @@ extension ServerInterceptor where Self == RequestCountingServerInterceptor {

/// Rejects all RPCs with the provided error.
struct RejectAllServerInterceptor: ServerInterceptor {
/// The error to reject all RPCs with.
let error: RPCError
/// Whether the error should be thrown. If `false` then the request is rejected with the error
/// instead.
let `throw`: Bool
enum Mode: Sendable {
/// Throw the error rather.
case `throw`(any Error)
/// Reject the RPC with a given error.
case reject(RPCError)
}

let mode: Mode

init(throw error: any Error) {
self.mode = .throw(error)
}

init(error: RPCError, throw: Bool = false) {
self.error = error
self.`throw` = `throw`
init(reject error: RPCError) {
self.mode = .reject(error)
}

func intercept<Input: Sendable, Output: Sendable>(
@@ -53,10 +59,11 @@ struct RejectAllServerInterceptor: ServerInterceptor {
ServerContext
) async throws -> StreamingServerResponse<Output>
) async throws -> StreamingServerResponse<Output> {
if self.throw {
throw self.error
} else {
return StreamingServerResponse(error: self.error)
switch self.mode {
case .throw(let error):
throw error
case .reject(let error):
return StreamingServerResponse(error: error)
}
}
}