#region Copyright notice and license // Copyright 2015-2016 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #endregion using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Google.Protobuf; using Grpc.Core; using Grpc.Core.Utils; using Grpc.Testing; using NUnit.Framework; namespace Grpc.IntegrationTesting { /// /// Shows how to attach custom error details as a binary trailer. /// public class CustomErrorDetailsTest { const string DebugInfoTrailerName = "debug-info-bin"; const string ExceptionDetail = "Exception thrown on purpose."; const string Host = "localhost"; Server server; Channel channel; TestService.TestServiceClient client; [OneTimeSetUp] public void Init() { // Disable SO_REUSEPORT to prevent https://github.com/grpc/grpc/issues/10755 server = new Server(new[] { new ChannelOption(ChannelOptions.SoReuseport, 0) }) { Services = { TestService.BindService(new CustomErrorDetailsTestServiceImpl()) }, Ports = { { Host, ServerPort.PickUnused, ServerCredentials.Insecure } } }; server.Start(); channel = new Channel(Host, server.Ports.Single().BoundPort, ChannelCredentials.Insecure); client = new TestService.TestServiceClient(channel); } [OneTimeTearDown] public void Cleanup() { channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); } [Test] public async Task ErrorDetailsFromCallObject() { var call = client.UnaryCallAsync(new SimpleRequest { ResponseSize = 10 }); try { await call.ResponseAsync; Assert.Fail(); } catch (RpcException e) { Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); var debugInfo = GetDebugInfo(call.GetTrailers()); Assert.AreEqual(debugInfo.Detail, ExceptionDetail); Assert.IsNotEmpty(debugInfo.StackEntries); } } [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); return DebugInfo.Parser.ParseFrom(entry.ValueBytes); } private class CustomErrorDetailsTestServiceImpl : TestService.TestServiceBase { public override Task UnaryCall(SimpleRequest request, ServerCallContext context) { try { throw new ArgumentException(ExceptionDetail); } catch (Exception e) { // Fill debug info with some structured details about the failure. var debugInfo = new DebugInfo(); debugInfo.Detail = e.Message; debugInfo.StackEntries.AddRange(e.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None)); context.ResponseTrailers.Add(DebugInfoTrailerName, debugInfo.ToByteArray()); throw new RpcException(new Status(StatusCode.Unknown, "The handler threw exception.")); } } } } }