Skip to content

Commit b82c0c7

Browse files
authored
Fix unobserved exception when using a client interceptor (#2082)
1 parent fb315ca commit b82c0c7

File tree

2 files changed

+173
-10
lines changed

2 files changed

+173
-10
lines changed

src/Grpc.Net.Client/Internal/GrpcCall.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -286,8 +286,12 @@ public Task<Metadata> GetResponseHeadersAsync()
286286
{
287287
if (_responseHeadersTask == null)
288288
{
289-
// Allocate metadata and task only when requested
289+
// Allocate metadata and task only when requested.
290290
_responseHeadersTask = GetResponseHeadersCoreAsync();
291+
292+
// ResponseHeadersAsync could be called inside a client interceptor when a call is wrapped.
293+
// Most people won't use the headers result. Observed exception to avoid unobserved exception event.
294+
_responseHeadersTask.ObserveException();
291295
}
292296

293297
return _responseHeadersTask;

test/Grpc.Net.Client.Tests/AsyncUnaryCallTests.cs

+167-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -20,6 +20,7 @@
2020
using System.Net.Http.Headers;
2121
using Greet;
2222
using Grpc.Core;
23+
using Grpc.Core.Interceptors;
2324
using Grpc.Net.Client.Internal;
2425
using Grpc.Net.Client.Tests.Infrastructure;
2526
using Grpc.Shared;
@@ -239,11 +240,12 @@ public enum ResponseHandleAction
239240
}
240241

241242
[Test]
242-
[TestCase(0, ResponseHandleAction.ResponseAsync)]
243-
[TestCase(0, ResponseHandleAction.ResponseHeadersAsync)]
244-
[TestCase(0, ResponseHandleAction.Dispose)]
245-
[TestCase(1, ResponseHandleAction.Nothing)]
246-
public async Task AsyncUnaryCall_CallFailed_NoUnobservedExceptions(int expectedUnobservedExceptions, ResponseHandleAction action)
243+
[TestCase(0, false, ResponseHandleAction.ResponseAsync)]
244+
[TestCase(0, true, ResponseHandleAction.ResponseAsync)]
245+
[TestCase(0, false, ResponseHandleAction.ResponseHeadersAsync)]
246+
[TestCase(0, false, ResponseHandleAction.Dispose)]
247+
[TestCase(1, false, ResponseHandleAction.Nothing)]
248+
public async Task AsyncUnaryCall_CallFailed_NoUnobservedExceptions(int expectedUnobservedExceptions, bool addClientInterceptor, ResponseHandleAction action)
247249
{
248250
// Arrange
249251
var services = new ServiceCollection();
@@ -267,7 +269,11 @@ public async Task AsyncUnaryCall_CallFailed_NoUnobservedExceptions(int expectedU
267269
{
268270
throw new Exception("Test error");
269271
});
270-
var invoker = HttpClientCallInvokerFactory.Create(httpClient, loggerFactory: loggerFactory);
272+
CallInvoker invoker = HttpClientCallInvokerFactory.Create(httpClient, loggerFactory: loggerFactory);
273+
if (addClientInterceptor)
274+
{
275+
invoker = invoker.Intercept(new ClientLoggerInterceptor(loggerFactory));
276+
}
271277

272278
// Act
273279
logger.LogDebug("Starting call");
@@ -282,7 +288,7 @@ public async Task AsyncUnaryCall_CallFailed_NoUnobservedExceptions(int expectedU
282288
// Assert
283289
Assert.AreEqual(expectedUnobservedExceptions, unobservedExceptions.Count);
284290

285-
static async Task MakeGrpcCallAsync(ILogger logger, HttpClientCallInvoker invoker, ResponseHandleAction action)
291+
static async Task MakeGrpcCallAsync(ILogger logger, CallInvoker invoker, ResponseHandleAction action)
286292
{
287293
var runTask = Task.Run(async () =>
288294
{
@@ -313,4 +319,157 @@ static async Task MakeGrpcCallAsync(ILogger logger, HttpClientCallInvoker invoke
313319
TaskScheduler.UnobservedTaskException -= onUnobservedTaskException;
314320
}
315321
}
322+
323+
private class ClientLoggerInterceptor : Interceptor
324+
{
325+
private readonly ILogger<ClientLoggerInterceptor> _logger;
326+
327+
public ClientLoggerInterceptor(ILoggerFactory loggerFactory)
328+
{
329+
_logger = loggerFactory.CreateLogger<ClientLoggerInterceptor>();
330+
}
331+
332+
public override TResponse BlockingUnaryCall<TRequest, TResponse>(
333+
TRequest request,
334+
ClientInterceptorContext<TRequest, TResponse> context,
335+
BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
336+
{
337+
LogCall(context.Method);
338+
AddCallerMetadata(ref context);
339+
340+
try
341+
{
342+
return continuation(request, context);
343+
}
344+
catch (Exception ex)
345+
{
346+
LogError(ex);
347+
throw;
348+
}
349+
}
350+
351+
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
352+
TRequest request,
353+
ClientInterceptorContext<TRequest, TResponse> context,
354+
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
355+
{
356+
LogCall(context.Method);
357+
AddCallerMetadata(ref context);
358+
359+
try
360+
{
361+
var call = continuation(request, context);
362+
363+
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
364+
}
365+
catch (Exception ex)
366+
{
367+
LogError(ex);
368+
throw;
369+
}
370+
}
371+
372+
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t)
373+
{
374+
try
375+
{
376+
var response = await t;
377+
_logger.LogInformation($"Response received: {response}");
378+
return response;
379+
}
380+
catch (Exception ex)
381+
{
382+
LogError(ex);
383+
throw;
384+
}
385+
}
386+
387+
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(
388+
ClientInterceptorContext<TRequest, TResponse> context,
389+
AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
390+
{
391+
LogCall(context.Method);
392+
AddCallerMetadata(ref context);
393+
394+
try
395+
{
396+
return continuation(context);
397+
}
398+
catch (Exception ex)
399+
{
400+
LogError(ex);
401+
throw;
402+
}
403+
}
404+
405+
public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(
406+
TRequest request,
407+
ClientInterceptorContext<TRequest, TResponse> context,
408+
AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
409+
{
410+
LogCall(context.Method);
411+
AddCallerMetadata(ref context);
412+
413+
try
414+
{
415+
return continuation(request, context);
416+
}
417+
catch (Exception ex)
418+
{
419+
LogError(ex);
420+
throw;
421+
}
422+
}
423+
424+
public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(
425+
ClientInterceptorContext<TRequest, TResponse> context,
426+
AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation)
427+
{
428+
LogCall(context.Method);
429+
AddCallerMetadata(ref context);
430+
431+
try
432+
{
433+
return continuation(context);
434+
}
435+
catch (Exception ex)
436+
{
437+
LogError(ex);
438+
throw;
439+
}
440+
}
441+
442+
private void LogCall<TRequest, TResponse>(Method<TRequest, TResponse> method)
443+
where TRequest : class
444+
where TResponse : class
445+
{
446+
_logger.LogInformation($"Starting call. Name: {method.Name}. Type: {method.Type}. Request: {typeof(TRequest)}. Response: {typeof(TResponse)}");
447+
}
448+
449+
private void AddCallerMetadata<TRequest, TResponse>(ref ClientInterceptorContext<TRequest, TResponse> context)
450+
where TRequest : class
451+
where TResponse : class
452+
{
453+
var headers = context.Options.Headers;
454+
455+
// Call doesn't have a headers collection to add to.
456+
// Need to create a new context with headers for the call.
457+
if (headers == null)
458+
{
459+
headers = new Metadata();
460+
var options = context.Options.WithHeaders(headers);
461+
context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context.Host, options);
462+
}
463+
464+
// Add caller metadata to call headers
465+
headers.Add("caller-user", Environment.UserName);
466+
headers.Add("caller-machine", Environment.MachineName);
467+
headers.Add("caller-os", Environment.OSVersion.ToString());
468+
}
469+
470+
private void LogError(Exception ex)
471+
{
472+
_logger.LogError(ex, $"Call error: {ex.Message}");
473+
}
474+
}
316475
}

0 commit comments

Comments
 (0)