From b201e4208dce8396594385cb7e65234a93c9dcba Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 16 Jun 2025 16:13:50 +0200 Subject: [PATCH 1/5] Convert errors thrown from interceptor inbound or outbound stream --- .../Internal/ClientResponse+Convenience.swift | 2 + .../Internal/ClientStreamExecutor.swift | 6 +- .../Server/Internal/ServerRPCExecutor.swift | 6 +- .../ClientRPCExecutorTestHarness.swift | 6 +- .../Internal/ClientRPCExecutorTests.swift | 42 ++++++++++ .../Internal/ServerRPCExecutorTests.swift | 44 ++++++++++ .../Call/Client/ClientInterceptors.swift | 42 ++++++++++ .../Call/Server/ServerInterceptors.swift | 80 +++++++++++++++++++ 8 files changed, 225 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift index 41c3d024..2b2abbab 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -71,6 +71,8 @@ extension ClientResponse { } catch let error as RPCError { // Known error type. self.accepted = .success(Contents(metadata: contents.metadata, error: error)) + } catch let error as any RPCErrorConvertible { + self.accepted = .success(Contents(metadata: contents.metadata, error: RPCError(error))) } catch { // Unexpected, but should be handled nonetheless. self.accepted = .failure(RPCError(code: .unknown, message: String(describing: error))) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 74aac103..2e4b78cb 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -96,7 +96,11 @@ internal enum ClientStreamExecutor { try await stream.write(.metadata(request.metadata)) try await request.producer(.map(into: stream) { .message(try serializer.serialize($0)) }) }.castError(to: RPCError.self) { other in - RPCError(code: .unknown, message: "Write failed.", cause: other) + if let convertible = other as? any RPCErrorConvertible { + RPCError(convertible) + } else { + RPCError(code: .unknown, message: "Write failed.", cause: other) + } } switch result { diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 8df70f86..096f528a 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -214,7 +214,11 @@ struct ServerRPCExecutor { .serializingToRPCResponsePart(into: outbound, with: serializer) ) }.castError(to: RPCError.self) { error in - RPCError(code: .unknown, message: "", cause: error) + if let convertible = error as? (any RPCErrorConvertible) { + return RPCError(convertible) + } else { + return RPCError(code: .unknown, message: "", cause: error) + } } switch result { diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 18a5d06c..770c92d8 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -132,7 +132,11 @@ struct ClientRPCExecutorTestHarness { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await self.serverTransport.listen { stream, context in - try? await self.server.handle(stream: stream) + do { + try await self.server.handle(stream: stream) + } catch { + await stream.outbound.finish(throwing: error) + } } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 474640f9..46e2cf75 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -290,4 +290,46 @@ final class ClientRPCExecutorTests: XCTestCase { } } } + + func testInterceptorProducerErrorConversion() 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: [.throwInProducer(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"]) + } + } + } + + func testInterceptorBodyPartsErrorConversion() 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: [.throwInBodyParts(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"]) + } + } + } } diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 73f9ba82..55830a59 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -394,4 +394,48 @@ final class ServerRPCExecutorTests: XCTestCase { XCTAssertEqual(parts, [.status(status, metadata)]) } } + + func testInterceptorProducerErrorConversion() async throws { + struct CustomError: RPCErrorConvertible, Error { + var rpcErrorCode: RPCError.Code { .alreadyExists } + var rpcErrorMessage: String { "foobar" } + var rpcErrorMetadata: Metadata { ["error": "yes"] } + } + + let harness = ServerRPCExecutorTestHarness( + interceptors: [.throwInProducer(CustomError(), after: .milliseconds(10))] + ) + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + try await inbound.write(.message([0])) + try await Task.sleep(for: .milliseconds(50)) + try await inbound.write(.message([1])) + 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, [.metadata(["foo": "bar"]), .message([0]), .status(status, metadata)]) + } + } + + func testInterceptorMessagesErrorConversion() async throws { + struct CustomError: RPCErrorConvertible, Error { + var rpcErrorCode: RPCError.Code { .alreadyExists } + var rpcErrorMessage: String { "foobar" } + var rpcErrorMetadata: Metadata { ["error": "yes"] } + } + + let harness = ServerRPCExecutorTestHarness(interceptors: [.throwInMessageSequence(CustomError())]) + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + try await inbound.write(.message([0])) // the sequence throws instantly, this should not arrive + 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, [.metadata(["foo": "bar"]), .status(status, metadata)]) + } + } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index 3c46a35d..96ae0dce 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -26,6 +26,13 @@ extension ClientInterceptor where Self == RejectAllClientInterceptor { return RejectAllClientInterceptor(throw: error) } + static func throwInBodyParts(_ error: any Error) -> Self { + return RejectAllClientInterceptor(throwInBodyParts: error) + } + + static func throwInProducer(_ error: any Error) -> Self { + return RejectAllClientInterceptor(throwInProducer: error) + } } @available(gRPCSwift 2.0, *) @@ -43,6 +50,10 @@ struct RejectAllClientInterceptor: ClientInterceptor { case `throw`(any Error) /// Reject the RPC with a given error. case reject(RPCError) + /// Throw an error in the body parts sequence. + case throwInBodyParts(any Error) + /// Throw an error in the message producer closure. + case throwInProducer(any Error) } let mode: Mode @@ -55,6 +66,14 @@ struct RejectAllClientInterceptor: ClientInterceptor { self.mode = .reject(error) } + init(throwInBodyParts error: any Error) { + self.mode = .throwInBodyParts(error) + } + + init(throwInProducer error: any Error) { + self.mode = .throwInProducer(error) + } + func intercept( request: StreamingClientRequest, context: ClientContext, @@ -68,6 +87,29 @@ struct RejectAllClientInterceptor: ClientInterceptor { throw error case .reject(let error): return StreamingClientResponse(error: error) + case .throwInBodyParts(let error): + var response = try await next(request, context) + switch response.accepted { + case .success(var success): + let stream = AsyncThrowingStream.Contents.BodyPart, any Error>.makeStream() + stream.continuation.finish(throwing: error) + + success.bodyParts = RPCAsyncSequence(wrapping: stream.stream) + response.accepted = .success(success) + return response + case .failure: + return response + } + case .throwInProducer(let error): + let wrappedProducer = request.producer + + var request = request + request.producer = { writer in + try await wrappedProducer(writer) + throw error + } + + return try await next(request, context) } } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 5918102d..dbb35aeb 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -25,6 +25,14 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { static func throwError(_ error: any Error) -> Self { RejectAllServerInterceptor(throw: error) } + + static func throwInProducer(_ error: any Error, after duration: Duration) -> Self { + RejectAllServerInterceptor(throwInProducer: error, after: duration) + } + + static func throwInMessageSequence(_ error: any Error) -> Self { + RejectAllServerInterceptor(throwInMessageSequence: error) + } } @available(gRPCSwift 2.0, *) @@ -42,6 +50,16 @@ struct RejectAllServerInterceptor: ServerInterceptor { case `throw`(any Error) /// Reject the RPC with a given error. case reject(RPCError) + /// Throw in the producer closure returned. + case throwInProducer(any Error, after: Duration) + /// Throw in the async sequence that stream inbound messages. + case throwInMessageSequence(any Error) + } + + private enum TimeoutResult { + case `throw`(any Error) + case cancelled + case result(Metadata) } let mode: Mode @@ -54,6 +72,14 @@ struct RejectAllServerInterceptor: ServerInterceptor { self.mode = .reject(error) } + init(throwInProducer error: any Error, after duration: Duration) { + self.mode = .throwInProducer(error, after: duration) + } + + init(throwInMessageSequence error: any Error) { + self.mode = .throwInMessageSequence(error) + } + func intercept( request: StreamingServerRequest, context: ServerContext, @@ -67,6 +93,60 @@ struct RejectAllServerInterceptor: ServerInterceptor { throw error case .reject(let error): return StreamingServerResponse(error: error) + case .throwInProducer(let error, let duration): + var response = try await next(request, context) + switch response.accepted { + case .success(var success): + let wrappedProducer = success.producer + success.producer = { writer in + let result: Result = await withTaskGroup(of: TimeoutResult.self) { group in + group.addTask { + do { + try await Task.sleep(for: duration, tolerance: .nanoseconds(1)) + } catch { + return .cancelled + } + return .throw(error) + } + + group.addTask { + do { + return .result(try await wrappedProducer(writer)) + } catch { + return .throw(error) + } + } + + let first = await group.next()! + group.cancelAll() + let second = await group.next()! + + switch (first, second) { + case (.throw(let error), _): + return .failure(error) + case (.result(let metadata), _): + return .success(metadata) + case (.cancelled, _): + return .failure(CancellationError()) + } + } + + return try result.get() + } + + response.accepted = .success(success) + return response + case .failure: + return response + } + case .throwInMessageSequence(let error): + let stream = AsyncThrowingStream.makeStream() + stream.continuation.finish(throwing: error) + + var request = request + request.messages = RPCAsyncSequence(wrapping: stream.stream) + + return try await next(request, context) } } } From c89db83604af2fded8d8e48f9fa9c7294832ea8f Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 18 Jun 2025 14:49:20 +0200 Subject: [PATCH 2/5] Introduce castOrConvertRPCError helper --- .../Internal/ClientStreamExecutor.swift | 10 +++------ .../Server/Internal/ServerRPCExecutor.swift | 8 ++----- .../GRPCCore/Internal/Result+Catching.swift | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 2e4b78cb..67bcc0a5 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -26,7 +26,7 @@ internal enum ClientStreamExecutor { /// - attempt: The attempt number for the RPC that will be executed. /// - serializer: A request serializer. /// - deserializer: A response deserializer. - /// - stream: The stream to excecute the RPC on. + /// - stream: The stream to execute the RPC on. /// - Returns: A streamed response. @inlinable static func execute( @@ -95,12 +95,8 @@ internal enum ClientStreamExecutor { let result = await Result { try await stream.write(.metadata(request.metadata)) try await request.producer(.map(into: stream) { .message(try serializer.serialize($0)) }) - }.castError(to: RPCError.self) { other in - if let convertible = other as? any RPCErrorConvertible { - RPCError(convertible) - } else { - RPCError(code: .unknown, message: "Write failed.", cause: other) - } + }.castOrConvertRPCError { other in + RPCError(code: .unknown, message: "Write failed.", cause: other) } switch result { diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 096f528a..a10c6211 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -213,12 +213,8 @@ struct ServerRPCExecutor { return try await contents.producer( .serializingToRPCResponsePart(into: outbound, with: serializer) ) - }.castError(to: RPCError.self) { error in - if let convertible = error as? (any RPCErrorConvertible) { - return RPCError(convertible) - } else { - return RPCError(code: .unknown, message: "", cause: error) - } + }.castOrConvertRPCError { error in + RPCError(code: .unknown, message: "", cause: error) } switch result { diff --git a/Sources/GRPCCore/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift index 07258ca5..28bf94f3 100644 --- a/Sources/GRPCCore/Internal/Result+Catching.swift +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -45,4 +45,25 @@ extension Result { return (error as? NewError) ?? buildError(error) } } + + /// Attempt to map or convert the error to an `RPCError`. + /// + /// If the cast or conversion is not possible then the provided closure is used to create an error of the given type. + /// + /// - Parameter buildError: A closure which constructs the desired error if conversion is not possible. + @inlinable + @available(gRPCSwift 2.0, *) + func castOrConvertRPCError( + or buildError: (any Error) -> RPCError + ) -> Result { + return self.mapError { error in + if let rpcError = error as? RPCError { + return rpcError + } else if let convertibleError = error as? any RPCErrorConvertible { + return RPCError(convertibleError) + } else { + return buildError(error) + } + } + } } From 5f26c53a06ed51e4cbd254aab9bd2124d72e97ff Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 18 Jun 2025 14:49:42 +0200 Subject: [PATCH 3/5] Simplify unit tests --- .../Internal/ServerRPCExecutorTests.swift | 5 +-- .../Call/Server/ServerInterceptors.swift | 44 +++++-------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 55830a59..61dd4fbf 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -403,14 +403,11 @@ final class ServerRPCExecutorTests: XCTestCase { } let harness = ServerRPCExecutorTestHarness( - interceptors: [.throwInProducer(CustomError(), after: .milliseconds(10))] + interceptors: [.throwInProducer(CustomError())] ) try await harness.execute(handler: .echo) { inbound in try await inbound.write(.metadata(["foo": "bar"])) try await inbound.write(.message([0])) - try await Task.sleep(for: .milliseconds(50)) - try await inbound.write(.message([1])) - await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() let status = Status(code: .alreadyExists, message: "foobar") diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index dbb35aeb..1372c6d8 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -26,8 +26,8 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { RejectAllServerInterceptor(throw: error) } - static func throwInProducer(_ error: any Error, after duration: Duration) -> Self { - RejectAllServerInterceptor(throwInProducer: error, after: duration) + static func throwInProducer(_ error: any Error) -> Self { + RejectAllServerInterceptor(throwInProducer: error) } static func throwInMessageSequence(_ error: any Error) -> Self { @@ -51,7 +51,7 @@ struct RejectAllServerInterceptor: ServerInterceptor { /// Reject the RPC with a given error. case reject(RPCError) /// Throw in the producer closure returned. - case throwInProducer(any Error, after: Duration) + case throwInProducer(any Error) /// Throw in the async sequence that stream inbound messages. case throwInMessageSequence(any Error) } @@ -72,8 +72,8 @@ struct RejectAllServerInterceptor: ServerInterceptor { self.mode = .reject(error) } - init(throwInProducer error: any Error, after duration: Duration) { - self.mode = .throwInProducer(error, after: duration) + init(throwInProducer error: any Error) { + self.mode = .throwInProducer(error) } init(throwInMessageSequence error: any Error) { @@ -93,45 +93,21 @@ struct RejectAllServerInterceptor: ServerInterceptor { throw error case .reject(let error): return StreamingServerResponse(error: error) - case .throwInProducer(let error, let duration): + case .throwInProducer(let error): var response = try await next(request, context) switch response.accepted { case .success(var success): let wrappedProducer = success.producer success.producer = { writer in - let result: Result = await withTaskGroup(of: TimeoutResult.self) { group in + try await withThrowingTaskGroup(of: Metadata.self) { group in group.addTask { - do { - try await Task.sleep(for: duration, tolerance: .nanoseconds(1)) - } catch { - return .cancelled - } - return .throw(error) + try await wrappedProducer(writer) } - group.addTask { - do { - return .result(try await wrappedProducer(writer)) - } catch { - return .throw(error) - } - } - - let first = await group.next()! group.cancelAll() - let second = await group.next()! - - switch (first, second) { - case (.throw(let error), _): - return .failure(error) - case (.result(let metadata), _): - return .success(metadata) - case (.cancelled, _): - return .failure(CancellationError()) - } + _ = try await group.next()! + throw error } - - return try result.get() } response.accepted = .success(success) From 2222adb6a2a593f905e80456cbd2957bdb75bf15 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 18 Jun 2025 14:56:03 +0200 Subject: [PATCH 4/5] Use castOrConvert in another palce and add unit tests --- .../Server/Internal/ServerRPCExecutor.swift | 16 ++++----- .../GRPCCore/Internal/Result+Catching.swift | 8 ++--- .../Internal/Result+CatchingTests.swift | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index a10c6211..96c929a4 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -188,16 +188,12 @@ struct ServerRPCExecutor { ) { request, context in try await handler(request, context) } - }.castError(to: RPCError.self) { error in - if let convertible = error as? (any RPCErrorConvertible) { - return RPCError(convertible) - } else { - return RPCError( - code: .unknown, - message: "Service method threw an unknown error.", - cause: error - ) - } + }.castOrConvertRPCError { error in + RPCError( + code: .unknown, + message: "Service method threw an unknown error.", + cause: error + ) }.flatMap { response in response.accepted } diff --git a/Sources/GRPCCore/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift index 28bf94f3..a3a0e358 100644 --- a/Sources/GRPCCore/Internal/Result+Catching.swift +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -56,11 +56,9 @@ extension Result { func castOrConvertRPCError( or buildError: (any Error) -> RPCError ) -> Result { - return self.mapError { error in - if let rpcError = error as? RPCError { - return rpcError - } else if let convertibleError = error as? any RPCErrorConvertible { - return RPCError(convertibleError) + return self.castError(to: RPCError.self) { error in + if let convertible = error as? any RPCErrorConvertible { + return RPCError(convertible) } else { return buildError(error) } diff --git a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift index 644bc72d..ce12f366 100644 --- a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift +++ b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift @@ -63,4 +63,39 @@ final class ResultCatchingTests: XCTestCase { XCTAssertEqual(error, RPCError(code: .invalidArgument, message: "fallback")) } } + + func testCastOrConvertRPCErrorConvertible() { + struct ConvertibleError: Error, RPCErrorConvertible { + let rpcErrorCode: RPCError.Code = .unknown + let rpcErrorMessage = "foo" + } + + let result = Result.failure(ConvertibleError()) + let typedFailure = result.castOrConvertRPCError { _ in + XCTFail("buildError(_:) was called") + return RPCError(code: .failedPrecondition, message: "shouldn't happen") + } + + switch typedFailure { + case .success: + XCTFail() + case .failure(let error): + XCTAssertEqual(error, RPCError(code: .unknown, message: "foo")) + } + } + + func testCastOrConvertToErrorOfIncorrectType() async { + struct WrongError: Error {} + let result = Result.failure(WrongError()) + let typedFailure = result.castOrConvertRPCError { _ in + return RPCError(code: .invalidArgument, message: "fallback") + } + + switch typedFailure { + case .success: + XCTFail() + case .failure(let error): + XCTAssertEqual(error, RPCError(code: .invalidArgument, message: "fallback")) + } + } } From 7c2528edd2dc3ab911115875178c3f1c6bc509df Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 18 Jun 2025 15:21:50 +0200 Subject: [PATCH 5/5] Format and lint --- .../Call/Server/Internal/ServerRPCExecutorTests.swift | 7 +++++-- .../Test Utilities/Call/Client/ClientInterceptors.swift | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 61dd4fbf..d094445b 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -423,10 +423,13 @@ final class ServerRPCExecutorTests: XCTestCase { var rpcErrorMetadata: Metadata { ["error": "yes"] } } - let harness = ServerRPCExecutorTestHarness(interceptors: [.throwInMessageSequence(CustomError())]) + let harness = ServerRPCExecutorTestHarness(interceptors: [ + .throwInMessageSequence(CustomError()) + ]) try await harness.execute(handler: .echo) { inbound in try await inbound.write(.metadata(["foo": "bar"])) - try await inbound.write(.message([0])) // the sequence throws instantly, this should not arrive + // the sequence throws instantly, this should not arrive + try await inbound.write(.message([0])) await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index 96ae0dce..95f18edd 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -91,7 +91,9 @@ struct RejectAllClientInterceptor: ClientInterceptor { var response = try await next(request, context) switch response.accepted { case .success(var success): - let stream = AsyncThrowingStream.Contents.BodyPart, any Error>.makeStream() + let stream = AsyncThrowingStream< + StreamingClientResponse.Contents.BodyPart, any Error + >.makeStream() stream.continuation.finish(throwing: error) success.bodyParts = RPCAsyncSequence(wrapping: stream.stream)