Skip to content
Snippets Groups Projects
Commit a48f879a authored by Jan Tattermusch's avatar Jan Tattermusch Committed by GitHub
Browse files

Merge pull request #12120 from jtattermusch/csharp_rpcexception

Add Trailers property to RpcException
parents 0fcd99e1 9dc18216
No related branches found
No related tags found
No related merge requests found
...@@ -92,9 +92,33 @@ namespace Grpc.Core.Tests ...@@ -92,9 +92,33 @@ namespace Grpc.Core.Tests
var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode); Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode);
Assert.AreEqual(0, ex.Trailers.Count);
var ex2 = Assert.ThrowsAsync<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc")); var ex2 = Assert.ThrowsAsync<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode); Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode);
Assert.AreEqual(0, ex.Trailers.Count);
}
[Test]
public void UnaryCall_ServerHandlerThrowsRpcExceptionWithTrailers()
{
helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
{
var trailers = new Metadata { {"xyz", "xyz-value"} };
throw new RpcException(new Status(StatusCode.Unauthenticated, ""), trailers);
});
var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode);
Assert.AreEqual(1, ex.Trailers.Count);
Assert.AreEqual("xyz", ex.Trailers[0].Key);
Assert.AreEqual("xyz-value", ex.Trailers[0].Value);
var ex2 = Assert.ThrowsAsync<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode);
Assert.AreEqual(1, ex2.Trailers.Count);
Assert.AreEqual("xyz", ex2.Trailers[0].Key);
Assert.AreEqual("xyz-value", ex2.Trailers[0].Value);
} }
[Test] [Test]
...@@ -108,9 +132,34 @@ namespace Grpc.Core.Tests ...@@ -108,9 +132,34 @@ namespace Grpc.Core.Tests
var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode); Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode);
Assert.AreEqual(0, ex.Trailers.Count);
var ex2 = Assert.ThrowsAsync<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode);
Assert.AreEqual(0, ex2.Trailers.Count);
}
[Test]
public void UnaryCall_ServerHandlerSetsStatusAndTrailers()
{
helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) =>
{
context.Status = new Status(StatusCode.Unauthenticated, "");
context.ResponseTrailers.Add("xyz", "xyz-value");
return "";
});
var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex.Status.StatusCode);
Assert.AreEqual(1, ex.Trailers.Count);
Assert.AreEqual("xyz", ex.Trailers[0].Key);
Assert.AreEqual("xyz-value", ex.Trailers[0].Value);
var ex2 = Assert.ThrowsAsync<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc")); var ex2 = Assert.ThrowsAsync<RpcException>(async () => await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc"));
Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode); Assert.AreEqual(StatusCode.Unauthenticated, ex2.Status.StatusCode);
Assert.AreEqual(1, ex2.Trailers.Count);
Assert.AreEqual("xyz", ex2.Trailers[0].Key);
Assert.AreEqual("xyz-value", ex2.Trailers[0].Value);
} }
[Test] [Test]
...@@ -148,7 +197,7 @@ namespace Grpc.Core.Tests ...@@ -148,7 +197,7 @@ namespace Grpc.Core.Tests
CollectionAssert.AreEqual(new string[] { "A", "B", "C" }, await call.ResponseStream.ToListAsync()); CollectionAssert.AreEqual(new string[] { "A", "B", "C" }, await call.ResponseStream.ToListAsync());
Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode); Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode);
Assert.IsNotNull("xyz", call.GetTrailers()[0].Key); Assert.AreEqual("xyz", call.GetTrailers()[0].Key);
} }
[Test] [Test]
...@@ -182,6 +231,27 @@ namespace Grpc.Core.Tests ...@@ -182,6 +231,27 @@ namespace Grpc.Core.Tests
Assert.AreEqual(StatusCode.InvalidArgument, ex2.Status.StatusCode); Assert.AreEqual(StatusCode.InvalidArgument, ex2.Status.StatusCode);
} }
[Test]
public async Task ServerStreamingCall_TrailersFromMultipleSourcesGetConcatenated()
{
helper.ServerStreamingHandler = new ServerStreamingServerMethod<string, string>(async (request, responseStream, context) =>
{
context.ResponseTrailers.Add("xyz", "xyz-value");
throw new RpcException(new Status(StatusCode.InvalidArgument, ""), new Metadata { {"abc", "abc-value"} });
});
var call = Calls.AsyncServerStreamingCall(helper.CreateServerStreamingCall(), "");
var ex = Assert.ThrowsAsync<RpcException>(async () => await call.ResponseStream.MoveNext());
Assert.AreEqual(StatusCode.InvalidArgument, ex.Status.StatusCode);
Assert.AreEqual(2, call.GetTrailers().Count);
Assert.AreEqual(2, ex.Trailers.Count);
Assert.AreEqual("xyz", ex.Trailers[0].Key);
Assert.AreEqual("xyz-value", ex.Trailers[0].Value);
Assert.AreEqual("abc", ex.Trailers[1].Key);
Assert.AreEqual("abc-value", ex.Trailers[1].Value);
}
[Test] [Test]
public async Task DuplexStreamingCall() public async Task DuplexStreamingCall()
{ {
...@@ -199,7 +269,7 @@ namespace Grpc.Core.Tests ...@@ -199,7 +269,7 @@ namespace Grpc.Core.Tests
CollectionAssert.AreEqual(new string[] { "A", "B", "C" }, await call.ResponseStream.ToListAsync()); CollectionAssert.AreEqual(new string[] { "A", "B", "C" }, await call.ResponseStream.ToListAsync());
Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode); Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode);
Assert.IsNotNull("xyz-value", call.GetTrailers()[0].Value); Assert.AreEqual("xyz-value", call.GetTrailers()[0].Value);
} }
[Test] [Test]
......
...@@ -329,7 +329,7 @@ namespace Grpc.Core.Internal ...@@ -329,7 +329,7 @@ namespace Grpc.Core.Internal
protected override Exception GetRpcExceptionClientOnly() protected override Exception GetRpcExceptionClientOnly()
{ {
return new RpcException(finishedStatus.Value.Status); return new RpcException(finishedStatus.Value.Status, finishedStatus.Value.Trailers);
} }
protected override Task CheckSendAllowedOrEarlyResult() protected override Task CheckSendAllowedOrEarlyResult()
...@@ -348,7 +348,7 @@ namespace Grpc.Core.Internal ...@@ -348,7 +348,7 @@ namespace Grpc.Core.Internal
// Writing after the call has finished is not a programming error because server can close // Writing after the call has finished is not a programming error because server can close
// the call anytime, so don't throw directly, but let the write task finish with an error. // the call anytime, so don't throw directly, but let the write task finish with an error.
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();
tcs.SetException(new RpcException(finishedStatus.Value.Status)); tcs.SetException(new RpcException(finishedStatus.Value.Status, finishedStatus.Value.Trailers));
return tcs.Task; return tcs.Task;
} }
...@@ -468,7 +468,7 @@ namespace Grpc.Core.Internal ...@@ -468,7 +468,7 @@ namespace Grpc.Core.Internal
var status = receivedStatus.Status; var status = receivedStatus.Status;
if (status.StatusCode != StatusCode.OK) if (status.StatusCode != StatusCode.OK)
{ {
unaryResponseTcs.SetException(new RpcException(status)); unaryResponseTcs.SetException(new RpcException(status, receivedStatus.Trailers));
return; return;
} }
...@@ -506,7 +506,7 @@ namespace Grpc.Core.Internal ...@@ -506,7 +506,7 @@ namespace Grpc.Core.Internal
var status = receivedStatus.Status; var status = receivedStatus.Status;
if (status.StatusCode != StatusCode.OK) if (status.StatusCode != StatusCode.OK)
{ {
streamingResponseCallFinishedTcs.SetException(new RpcException(status)); streamingResponseCallFinishedTcs.SetException(new RpcException(status, receivedStatus.Trailers));
return; return;
} }
......
...@@ -76,7 +76,7 @@ namespace Grpc.Core.Internal ...@@ -76,7 +76,7 @@ namespace Grpc.Core.Internal
{ {
Logger.Warning(e, "Exception occured in handler."); Logger.Warning(e, "Exception occured in handler.");
} }
status = HandlerUtils.StatusFromException(e); status = HandlerUtils.GetStatusFromExceptionAndMergeTrailers(e, context.ResponseTrailers);
} }
try try
{ {
...@@ -133,7 +133,7 @@ namespace Grpc.Core.Internal ...@@ -133,7 +133,7 @@ namespace Grpc.Core.Internal
{ {
Logger.Warning(e, "Exception occured in handler."); Logger.Warning(e, "Exception occured in handler.");
} }
status = HandlerUtils.StatusFromException(e); status = HandlerUtils.GetStatusFromExceptionAndMergeTrailers(e, context.ResponseTrailers);
} }
try try
...@@ -191,7 +191,7 @@ namespace Grpc.Core.Internal ...@@ -191,7 +191,7 @@ namespace Grpc.Core.Internal
{ {
Logger.Warning(e, "Exception occured in handler."); Logger.Warning(e, "Exception occured in handler.");
} }
status = HandlerUtils.StatusFromException(e); status = HandlerUtils.GetStatusFromExceptionAndMergeTrailers(e, context.ResponseTrailers);
} }
try try
...@@ -247,7 +247,7 @@ namespace Grpc.Core.Internal ...@@ -247,7 +247,7 @@ namespace Grpc.Core.Internal
{ {
Logger.Warning(e, "Exception occured in handler."); Logger.Warning(e, "Exception occured in handler.");
} }
status = HandlerUtils.StatusFromException(e); status = HandlerUtils.GetStatusFromExceptionAndMergeTrailers(e, context.ResponseTrailers);
} }
try try
{ {
...@@ -292,11 +292,20 @@ namespace Grpc.Core.Internal ...@@ -292,11 +292,20 @@ namespace Grpc.Core.Internal
internal static class HandlerUtils internal static class HandlerUtils
{ {
public static Status StatusFromException(Exception e) public static Status GetStatusFromExceptionAndMergeTrailers(Exception e, Metadata callContextResponseTrailers)
{ {
var rpcException = e as RpcException; var rpcException = e as RpcException;
if (rpcException != null) if (rpcException != null)
{ {
// There are two sources of metadata entries on the server-side:
// 1. serverCallContext.ResponseTrailers
// 2. trailers in RpcException thrown by user code in server side handler.
// As metadata allows duplicate keys, the logical thing to do is
// to just merge trailers from RpcException into serverCallContext.ResponseTrailers.
foreach (var entry in rpcException.Trailers)
{
callContextResponseTrailers.Add(entry);
}
// use the status thrown by handler. // use the status thrown by handler.
return rpcException.Status; return rpcException.Status;
} }
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
#endregion #endregion
using System; using System;
using Grpc.Core.Utils;
namespace Grpc.Core namespace Grpc.Core
{ {
...@@ -26,6 +27,7 @@ namespace Grpc.Core ...@@ -26,6 +27,7 @@ namespace Grpc.Core
public class RpcException : Exception public class RpcException : Exception
{ {
private readonly Status status; private readonly Status status;
private readonly Metadata trailers;
/// <summary> /// <summary>
/// Creates a new <c>RpcException</c> associated with given status. /// Creates a new <c>RpcException</c> associated with given status.
...@@ -34,6 +36,7 @@ namespace Grpc.Core ...@@ -34,6 +36,7 @@ namespace Grpc.Core
public RpcException(Status status) : base(status.ToString()) public RpcException(Status status) : base(status.ToString())
{ {
this.status = status; this.status = status;
this.trailers = Metadata.Empty;
} }
/// <summary> /// <summary>
...@@ -44,6 +47,18 @@ namespace Grpc.Core ...@@ -44,6 +47,18 @@ namespace Grpc.Core
public RpcException(Status status, string message) : base(message) public RpcException(Status status, string message) : base(message)
{ {
this.status = status; this.status = status;
this.trailers = Metadata.Empty;
}
/// <summary>
/// Creates a new <c>RpcException</c> associated with given status and trailing response metadata.
/// </summary>
/// <param name="status">Resulting status of a call.</param>
/// <param name="trailers">Response trailing metadata.</param>
public RpcException(Status status, Metadata trailers) : base(status.ToString())
{
this.status = status;
this.trailers = GrpcPreconditions.CheckNotNull(trailers);
} }
/// <summary> /// <summary>
...@@ -56,5 +71,18 @@ namespace Grpc.Core ...@@ -56,5 +71,18 @@ namespace Grpc.Core
return status; return status;
} }
} }
/// <summary>
/// Gets the call trailing metadata.
/// Trailers only have meaningful content for client-side calls (in which case they represent the trailing metadata sent by the server when closing the call).
/// Instances of <c>RpcException</c> thrown by the server-side part of the stack will have trailers always set to empty.
/// </summary>
public Metadata Trailers
{
get
{
return trailers;
}
}
} }
} }
...@@ -65,7 +65,7 @@ namespace Grpc.IntegrationTesting ...@@ -65,7 +65,7 @@ namespace Grpc.IntegrationTesting
} }
[Test] [Test]
public async Task UnaryCall() public async Task ErrorDetailsFromCallObject()
{ {
var call = client.UnaryCallAsync(new SimpleRequest { ResponseSize = 10 }); var call = client.UnaryCallAsync(new SimpleRequest { ResponseSize = 10 });
...@@ -83,7 +83,24 @@ namespace Grpc.IntegrationTesting ...@@ -83,7 +83,24 @@ namespace Grpc.IntegrationTesting
} }
} }
private DebugInfo GetDebugInfo(Metadata trailers) [Test]
public async Task ErrorDetailsFromRpcException()
{
try
{
await client.UnaryCallAsync(new SimpleRequest { ResponseSize = 10 });
Assert.Fail();
}
catch (RpcException e)
{
Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode);
var debugInfo = GetDebugInfo(e.Trailers);
Assert.AreEqual(debugInfo.Detail, ExceptionDetail);
Assert.IsNotEmpty(debugInfo.StackEntries);
}
}
private static DebugInfo GetDebugInfo(Metadata trailers)
{ {
var entry = trailers.First((e) => e.Key == DebugInfoTrailerName); var entry = trailers.First((e) => e.Key == DebugInfoTrailerName);
return DebugInfo.Parser.ParseFrom(entry.ValueBytes); return DebugInfo.Parser.ParseFrom(entry.ValueBytes);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment