Skip to content

Commit c4d6281

Browse files
authored
Convert errors thrown from interceptors (#2209)
Motivation: gRPC checks whether errors thrown from interceptors are `RPCError` and otherwise treats them as `unknown` (to avoid leaking internal information). There is a third possibility: the error is explicitly marked as being convertible to an `RPCError`. This check is currently missing when thrown from client/server interceptors. Modifications: - Catch `RPCErrorConvertible` in the client/server executors when thrown from interceptors - Add tests Result: Error information isn't dropped
1 parent 1470451 commit c4d6281

File tree

7 files changed

+96
-32
lines changed

7 files changed

+96
-32
lines changed

Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift

+2
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ extension ClientRPCExecutor {
186186
}
187187
} catch let error as RPCError {
188188
return StreamingClientResponse(error: error)
189+
} catch let error as RPCErrorConvertible {
190+
return StreamingClientResponse(error: RPCError(error))
189191
} catch let other {
190192
let error = RPCError(code: .unknown, message: "", cause: other)
191193
return StreamingClientResponse(error: error)

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

+2
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ extension ServerRPCExecutor {
330330
}
331331
} catch let error as RPCError {
332332
return StreamingServerResponse(error: error)
333+
} catch let error as RPCErrorConvertible {
334+
return StreamingServerResponse(error: RPCError(error))
333335
} catch let other {
334336
let error = RPCError(code: .unknown, message: "", cause: other)
335337
return StreamingServerResponse(error: error)

Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct ClientRPCExecutorTestHarness {
2929
private let server: ServerStreamHandler
3030
private let clientTransport: StreamCountingClientTransport
3131
private let serverTransport: StreamCountingServerTransport
32+
private let interceptors: [any ClientInterceptor]
3233

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

45-
init(transport: Transport = .inProcess, server: ServerStreamHandler) {
46+
init(
47+
transport: Transport = .inProcess,
48+
server: ServerStreamHandler,
49+
interceptors: [any ClientInterceptor] = []
50+
) {
4651
self.server = server
52+
self.interceptors = interceptors
4753

4854
switch transport {
4955
case .inProcess:
@@ -141,7 +147,7 @@ struct ClientRPCExecutorTestHarness {
141147
serializer: serializer,
142148
deserializer: deserializer,
143149
transport: self.clientTransport,
144-
interceptors: [],
150+
interceptors: self.interceptors,
145151
handler: handler
146152
)
147153

Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift

+21
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,25 @@ final class ClientRPCExecutorTests: XCTestCase {
268268
}
269269
}
270270
}
271+
272+
func testInterceptorErrorConversion() async throws {
273+
struct CustomError: RPCErrorConvertible, Error {
274+
var rpcErrorCode: RPCError.Code { .alreadyExists }
275+
var rpcErrorMessage: String { "foobar" }
276+
var rpcErrorMetadata: Metadata { ["error": "yes"] }
277+
}
278+
279+
let tester = ClientRPCExecutorTestHarness(
280+
server: .echo,
281+
interceptors: [.throwError(CustomError())]
282+
)
283+
284+
try await tester.unary(request: ClientRequest(message: [])) { response in
285+
XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in
286+
XCTAssertEqual(error.code, .alreadyExists)
287+
XCTAssertEqual(error.message, "foobar")
288+
XCTAssertEqual(error.metadata, ["error": "yes"])
289+
}
290+
}
291+
}
271292
}

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

+19
Original file line numberDiff line numberDiff line change
@@ -374,4 +374,23 @@ final class ServerRPCExecutorTests: XCTestCase {
374374
)
375375
}
376376
}
377+
378+
func testInterceptorErrorConversion() async throws {
379+
struct CustomError: RPCErrorConvertible, Error {
380+
var rpcErrorCode: RPCError.Code { .alreadyExists }
381+
var rpcErrorMessage: String { "foobar" }
382+
var rpcErrorMetadata: Metadata { ["error": "yes"] }
383+
}
384+
385+
let harness = ServerRPCExecutorTestHarness(interceptors: [.throwError(CustomError())])
386+
try await harness.execute(handler: .throwing(CustomError())) { inbound in
387+
try await inbound.write(.metadata(["foo": "bar"]))
388+
await inbound.finish()
389+
} consumer: { outbound in
390+
let parts = try await outbound.collect()
391+
let status = Status(code: .alreadyExists, message: "foobar")
392+
let metadata: Metadata = ["error": "yes"]
393+
XCTAssertEqual(parts, [.status(status, metadata)])
394+
}
395+
}
377396
}

Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift

+22-15
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import GRPCCore
1818

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

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

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

3636
/// Rejects all RPCs with the provided error.
3737
struct RejectAllClientInterceptor: ClientInterceptor {
38-
/// The error to reject all RPCs with.
39-
let error: RPCError
40-
/// Whether the error should be thrown. If `false` then the request is rejected with the error
41-
/// instead.
42-
let `throw`: Bool
38+
enum Mode: Sendable {
39+
/// Throw the error rather.
40+
case `throw`(any Error)
41+
/// Reject the RPC with a given error.
42+
case reject(RPCError)
43+
}
44+
45+
let mode: Mode
46+
47+
init(throw error: any Error) {
48+
self.mode = .throw(error)
49+
}
4350

44-
init(error: RPCError, throw: Bool = false) {
45-
self.error = error
46-
self.`throw` = `throw`
51+
init(reject error: RPCError) {
52+
self.mode = .reject(error)
4753
}
4854

4955
func intercept<Input: Sendable, Output: Sendable>(
@@ -54,10 +60,11 @@ struct RejectAllClientInterceptor: ClientInterceptor {
5460
ClientContext
5561
) async throws -> StreamingClientResponse<Output>
5662
) async throws -> StreamingClientResponse<Output> {
57-
if self.throw {
58-
throw self.error
59-
} else {
60-
return StreamingClientResponse(error: self.error)
63+
switch self.mode {
64+
case .throw(let error):
65+
throw error
66+
case .reject(let error):
67+
return StreamingClientResponse(error: error)
6168
}
6269
}
6370
}

Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift

+22-15
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import GRPCCore
1818

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

24-
static func throwError(_ error: RPCError) -> Self {
25-
RejectAllServerInterceptor(error: error, throw: true)
24+
static func throwError(_ error: any Error) -> Self {
25+
RejectAllServerInterceptor(throw: error)
2626
}
2727
}
2828

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

3535
/// Rejects all RPCs with the provided error.
3636
struct RejectAllServerInterceptor: ServerInterceptor {
37-
/// The error to reject all RPCs with.
38-
let error: RPCError
39-
/// Whether the error should be thrown. If `false` then the request is rejected with the error
40-
/// instead.
41-
let `throw`: Bool
37+
enum Mode: Sendable {
38+
/// Throw the error rather.
39+
case `throw`(any Error)
40+
/// Reject the RPC with a given error.
41+
case reject(RPCError)
42+
}
43+
44+
let mode: Mode
45+
46+
init(throw error: any Error) {
47+
self.mode = .throw(error)
48+
}
4249

43-
init(error: RPCError, throw: Bool = false) {
44-
self.error = error
45-
self.`throw` = `throw`
50+
init(reject error: RPCError) {
51+
self.mode = .reject(error)
4652
}
4753

4854
func intercept<Input: Sendable, Output: Sendable>(
@@ -53,10 +59,11 @@ struct RejectAllServerInterceptor: ServerInterceptor {
5359
ServerContext
5460
) async throws -> StreamingServerResponse<Output>
5561
) async throws -> StreamingServerResponse<Output> {
56-
if self.throw {
57-
throw self.error
58-
} else {
59-
return StreamingServerResponse(error: self.error)
62+
switch self.mode {
63+
case .throw(let error):
64+
throw error
65+
case .reject(let error):
66+
return StreamingServerResponse(error: error)
6067
}
6168
}
6269
}

0 commit comments

Comments
 (0)