Skip to content

Commit 703cc14

Browse files
committed
Provide alternative implementation of #75 via SimpleRpcExceptionsInterceptor
1 parent a8f43bf commit 703cc14

File tree

4 files changed

+175
-26
lines changed

4 files changed

+175
-26
lines changed

src/protobuf-net.Grpc/Configuration/SimpleRpcExceptionsAttribute.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ namespace ProtoBuf.Grpc.Configuration
99
/// done with caution as this may present security implications. Additional exception metadata (<see cref="Exception.Data"/>, <see cref="Exception.InnerException"/>,
1010
/// <see cref="Exception.StackTrace"/>, etc) is not propagated. The exception is still exposed at the client as an <see cref="RpcException"/>.
1111
/// </summary>
12-
/// <remarks>This feature is only currently supported on <c>async</c> methods that expose their faults via the returned awaitable, not by throwing directly.</remarks>
12+
/// <remarks>This feature is only currently supported on <c>async</c> methods that expose their faults via the returned awaitable, not by throwing directly; a more robust
13+
/// implementation is provided by the <see cref="SimpleRpcExceptionsInterceptor"/> interceptor.</remarks>
1314
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
1415
public sealed class SimpleRpcExceptionsAttribute : Attribute
1516
{
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using Grpc.Core;
2+
using Grpc.Core.Interceptors;
3+
using System;
4+
using System.IO;
5+
using System.Runtime.CompilerServices;
6+
using System.Security;
7+
using System.Threading.Tasks;
8+
9+
namespace ProtoBuf.Grpc.Configuration
10+
{
11+
/// <summary>
12+
/// Indicates that a service or method should use simplified exception handling - which means that all server exceptions are treated as <see cref="RpcException"/>; this
13+
/// will expose the <see cref="Exception.Message"/> to the caller (and the type may be interpreted as a <see cref="StatusCode"/> when possible), which should only be
14+
/// done with caution as this may present security implications. Additional exception metadata (<see cref="Exception.Data"/>, <see cref="Exception.InnerException"/>,
15+
/// <see cref="Exception.StackTrace"/>, etc) is not propagated. The exception is still exposed at the client as an <see cref="RpcException"/>.
16+
/// </summary>
17+
public sealed class SimpleRpcExceptionsInterceptor : Interceptor
18+
{
19+
private SimpleRpcExceptionsInterceptor() { }
20+
private static SimpleRpcExceptionsInterceptor? s_Instance;
21+
/// <summary>
22+
/// Provides a shared instance of this interceptor
23+
/// </summary>
24+
public static SimpleRpcExceptionsInterceptor Instance => s_Instance ??= new SimpleRpcExceptionsInterceptor();
25+
26+
/// <inheritdoc/>
27+
public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(IAsyncStreamReader<TRequest> requestStream, ServerCallContext context, ClientStreamingServerMethod<TRequest, TResponse> continuation)
28+
{
29+
try
30+
{
31+
return await base.ClientStreamingServerHandler(requestStream, context, continuation).ConfigureAwait(false);
32+
}
33+
catch (Exception ex) when (IsNotRpcException(ex))
34+
{
35+
RethrowAsRpcException(ex);
36+
return default!; // make compiler happy
37+
}
38+
}
39+
40+
/// <inheritdoc/>
41+
public override async Task ServerStreamingServerHandler<TRequest, TResponse>(TRequest request, IServerStreamWriter<TResponse> responseStream, ServerCallContext context, ServerStreamingServerMethod<TRequest, TResponse> continuation)
42+
{
43+
try
44+
{
45+
await base.ServerStreamingServerHandler(request, responseStream, context, continuation).ConfigureAwait(false);
46+
}
47+
catch (Exception ex) when (IsNotRpcException(ex))
48+
{
49+
RethrowAsRpcException(ex);
50+
}
51+
}
52+
53+
/// <inheritdoc/>
54+
public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(IAsyncStreamReader<TRequest> requestStream, IServerStreamWriter<TResponse> responseStream, ServerCallContext context, DuplexStreamingServerMethod<TRequest, TResponse> continuation)
55+
{
56+
try
57+
{
58+
await base.DuplexStreamingServerHandler(requestStream, responseStream, context, continuation).ConfigureAwait(false);
59+
}
60+
catch (Exception ex) when (IsNotRpcException(ex))
61+
{
62+
RethrowAsRpcException(ex);
63+
}
64+
}
65+
66+
/// <inheritdoc/>
67+
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
68+
{
69+
try
70+
{
71+
return await base.UnaryServerHandler(request, context, continuation).ConfigureAwait(false);
72+
}
73+
catch (Exception ex) when (IsNotRpcException(ex))
74+
{
75+
RethrowAsRpcException(ex);
76+
return default!; // make compiler happy
77+
}
78+
}
79+
80+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
81+
internal static bool IsNotRpcException(Exception ex) => !(ex is RpcException);
82+
83+
internal static void RethrowAsRpcException(Exception ex)
84+
{
85+
var code = ex switch
86+
{
87+
OperationCanceledException => StatusCode.Cancelled,
88+
ArgumentException => StatusCode.InvalidArgument,
89+
NotImplementedException => StatusCode.Unimplemented,
90+
SecurityException => StatusCode.PermissionDenied,
91+
EndOfStreamException => StatusCode.OutOfRange,
92+
FileNotFoundException => StatusCode.NotFound,
93+
DirectoryNotFoundException => StatusCode.NotFound,
94+
TimeoutException => StatusCode.DeadlineExceeded,
95+
_ => StatusCode.Unknown,
96+
};
97+
throw new RpcException(new Status(code, ex.Message), ex.Message);
98+
}
99+
}
100+
}

src/protobuf-net.Grpc/Internal/Reshape.cs

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using Grpc.Core;
2+
using ProtoBuf.Grpc.Configuration;
23
using System;
34
using System.Collections.Generic;
45
using System.ComponentModel;
5-
using System.IO;
66
using System.Runtime.CompilerServices;
7-
using System.Security;
87
using System.Threading;
98
using System.Threading.Tasks;
109
namespace ProtoBuf.Grpc.Internal
@@ -153,13 +152,15 @@ static async Task Awaited(Task task)
153152
{
154153
await task.ConfigureAwait(false);
155154
}
156-
catch (Exception ex) when (!(ex is RpcException))
155+
catch (Exception ex) when (SimpleRpcExceptionsInterceptor.IsNotRpcException(ex))
157156
{
158-
Rethrow(ex);
157+
SimpleRpcExceptionsInterceptor.RethrowAsRpcException(ex);
159158
}
160159
}
161160
}
162161

162+
163+
163164
/// <summary>
164165
/// Consumes the provided task raising exceptions as <see cref="RpcException"/>
165166
/// </summary>
@@ -175,31 +176,14 @@ static async Task<T> Awaited(Task<T> task)
175176
{
176177
return await task.ConfigureAwait(false);
177178
}
178-
catch (Exception ex) when (!(ex is RpcException))
179+
catch (Exception ex) when (SimpleRpcExceptionsInterceptor.IsNotRpcException(ex))
179180
{
180-
Rethrow(ex);
181+
SimpleRpcExceptionsInterceptor.RethrowAsRpcException(ex);
181182
return default!; // make compiler happy
182183
}
183184
}
184185
}
185186

186-
private static void Rethrow(Exception ex)
187-
{
188-
var code = ex switch
189-
{
190-
OperationCanceledException => StatusCode.Cancelled,
191-
ArgumentException => StatusCode.InvalidArgument,
192-
NotImplementedException => StatusCode.Unimplemented,
193-
SecurityException => StatusCode.PermissionDenied,
194-
EndOfStreamException => StatusCode.OutOfRange,
195-
FileNotFoundException => StatusCode.NotFound,
196-
DirectoryNotFoundException => StatusCode.NotFound,
197-
TimeoutException => StatusCode.DeadlineExceeded,
198-
_ => StatusCode.Unknown,
199-
};
200-
throw new RpcException(new Status(code, ex.Message), ex.Message);
201-
}
202-
203187
/// <summary>
204188
/// Performs a gRPC blocking unary call
205189
/// </summary>

tests/protobuf-net.Grpc.Test.Integration/Issues/Issue75_Exceptions.cs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ public interface IFaultTest
2727
ValueTask SimplifiedFaultHandling_Fault();
2828
}
2929

30+
[Service]
31+
public interface IInterceptedFaultTest
32+
{
33+
ValueTask Success();
34+
ValueTask Fault();
35+
}
36+
3037
public Issue75(Issue75ServerFixture _)
3138
{
3239
GrpcClientFactory.AllowUnencryptedHttp2 = true;
3340
}
3441

35-
public class Issue75ServerFixture : IFaultTest, IAsyncDisposable
42+
public class Issue75ServerFixture : IFaultTest, IInterceptedFaultTest, IAsyncDisposable
3643
{
3744
public ValueTask DisposeAsync() => new ValueTask(_server.KillAsync());
3845

@@ -43,7 +50,8 @@ public Issue75ServerFixture()
4350
{
4451
Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) }
4552
};
46-
_server.Services.AddCodeFirst(this);
53+
_server.Services.AddCodeFirst<IFaultTest>(this);
54+
_server.Services.AddCodeFirst<IInterceptedFaultTest>(this, interceptors: new[] { SimpleRpcExceptionsInterceptor.Instance });
4755
_server.Start();
4856
}
4957
async ValueTask IFaultTest.VanillaFaultHanding_Fault()
@@ -59,6 +67,43 @@ async ValueTask IFaultTest.SimplifiedFaultHandling_Fault()
5967
throw new ArgumentOutOfRangeException("foo");
6068
}
6169
ValueTask IFaultTest.SimplifiedFaultHandling_Success() => default;
70+
71+
ValueTask IInterceptedFaultTest.Success() => default;
72+
73+
ValueTask IInterceptedFaultTest.Fault() => throw new ArgumentOutOfRangeException("foo");
74+
}
75+
76+
[Fact]
77+
public async Task UnmanagedClient_Intercepted_Fault()
78+
{
79+
var channel = new Channel("localhost", Port, ChannelCredentials.Insecure);
80+
try
81+
{
82+
var client = channel.CreateGrpcService<IInterceptedFaultTest>();
83+
var ex = await Assert.ThrowsAsync<RpcException>(async () => await client.Fault());
84+
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
85+
Assert.Equal(s_ExpectedMessage, ex.Status.Detail);
86+
Assert.Equal($"Status(StatusCode=InvalidArgument, Detail=\"{s_ExpectedMessage}\")", ex.Message);
87+
}
88+
finally
89+
{
90+
await channel.ShutdownAsync();
91+
}
92+
}
93+
94+
[Fact]
95+
public async Task UnmanagedClient_Intercepted_Success()
96+
{
97+
var channel = new Channel("localhost", Port, ChannelCredentials.Insecure);
98+
try
99+
{
100+
var client = channel.CreateGrpcService<IInterceptedFaultTest>();
101+
await client.Success();
102+
}
103+
finally
104+
{
105+
await channel.ShutdownAsync();
106+
}
62107
}
63108

64109
[Fact]
@@ -166,6 +211,25 @@ public async Task ManagedClient_Simplified_Success()
166211
var client = http.CreateGrpcService<IFaultTest>();
167212
await client.SimplifiedFaultHandling_Success();
168213
}
214+
215+
[Fact]
216+
public async Task ManagedClient_Intercepted_Fault()
217+
{
218+
using var http = global::Grpc.Net.Client.GrpcChannel.ForAddress($"http://localhost:{Port}");
219+
var client = http.CreateGrpcService<IInterceptedFaultTest>();
220+
var ex = await Assert.ThrowsAsync<RpcException>(async () => await client.Fault());
221+
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
222+
Assert.Equal(s_ExpectedMessage, ex.Status.Detail);
223+
Assert.Equal($"Status(StatusCode=InvalidArgument, Detail=\"{s_ExpectedMessage}\")", ex.Message);
224+
}
225+
226+
[Fact]
227+
public async Task ManagedClient_Intercepted_Success()
228+
{
229+
using var http = global::Grpc.Net.Client.GrpcChannel.ForAddress($"http://localhost:{Port}");
230+
var client = http.CreateGrpcService<IInterceptedFaultTest>();
231+
await client.Success();
232+
}
169233
#endif
170234
}
171235
}

0 commit comments

Comments
 (0)