gRPC Interceptors — Cross-Cutting Concerns in gRPC
Build gRPC interceptors in ASP.NET Core: logging interceptors, authentication validation, error handling, retry policies on the client, and applying interceptors globally vs per-service.
What gRPC Interceptors Are
Interceptors are gRPC's equivalent of middleware. They wrap gRPC calls on both server and client sides, enabling cross-cutting concerns (logging, auth, error handling, metrics) without modifying service methods.
Server interceptor pipeline:
Request → ServerInterceptorA → ServerInterceptorB → ServiceMethod
Client interceptor pipeline:
ClientCode → ClientInterceptorA → ClientInterceptorB → Network → ServerServer-Side Logging Interceptor
// Infrastructure/Grpc/Interceptors/LoggingInterceptor.cs
public sealed class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
=> _logger = logger;
// Intercept unary calls
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var method = context.Method;
var user = context.GetHttpContext().User
.FindFirstValue(JwtRegisteredClaimNames.Sub);
var requestId = context.RequestHeaders
.FirstOrDefault(h => h.Key == "x-request-id")?.Value;
_logger.LogInformation(
"gRPC {Method} called by {UserId} [{RequestId}]",
method, user ?? "anonymous", requestId);
var sw = Stopwatch.StartNew();
try
{
var response = await continuation(request, context);
sw.Stop();
_logger.LogInformation(
"gRPC {Method} completed in {ElapsedMs}ms", method, sw.ElapsedMilliseconds);
return response;
}
catch (RpcException ex)
{
sw.Stop();
_logger.LogWarning(
"gRPC {Method} failed with {StatusCode} in {ElapsedMs}ms: {Detail}",
method, ex.StatusCode, sw.ElapsedMilliseconds, ex.Status.Detail);
throw;
}
catch (Exception ex)
{
sw.Stop();
_logger.LogError(ex,
"gRPC {Method} threw unhandled exception in {ElapsedMs}ms",
method, sw.ElapsedMilliseconds);
throw new RpcException(new Status(StatusCode.Internal, "An unexpected error occurred."));
}
}
// Intercept server streaming calls
public override Task ServerStreamingServerHandler<TRequest, TResponse>(
TRequest request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
ServerStreamingServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation("gRPC streaming {Method} started", context.Method);
return continuation(request, responseStream, context);
}
}Server-Side Error Handling Interceptor
public sealed class ErrorHandlingInterceptor : Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (RpcException)
{
throw; // let RpcExceptions through unchanged
}
catch (ValidationException ex)
{
throw new RpcException(new Status(StatusCode.InvalidArgument,
string.Join("; ", ex.Errors.Select(e => e.ErrorMessage))));
}
catch (NotFoundException ex)
{
throw new RpcException(new Status(StatusCode.NotFound, ex.Message));
}
catch (UnauthorizedAccessException)
{
throw new RpcException(new Status(StatusCode.PermissionDenied,
"Access denied."));
}
catch (Exception ex)
{
// Log internally — do not expose to client
GetLogger(context).LogError(ex, "Unhandled exception in gRPC service");
throw new RpcException(new Status(StatusCode.Internal,
"An unexpected error occurred. Ref: " + context.RequestHeaders
.FirstOrDefault(h => h.Key == "x-request-id")?.Value));
}
}
}Audit Interceptor for Clinical Systems
public sealed class ClinicalAuditInterceptor : Interceptor
{
private readonly IAuditLogger _audit;
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var user = context.GetHttpContext().User;
var userId = user.FindFirstValue(JwtRegisteredClaimNames.Sub);
var method = context.Method;
await _audit.RecordAsync(new AuditEntry(
UserId: userId ?? "unknown",
Action: method,
Timestamp: DateTime.UtcNow,
Source: "gRPC"));
return await continuation(request, context);
}
}Registering Server Interceptors
// Program.cs — apply to all gRPC services
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
options.Interceptors.Add<ErrorHandlingInterceptor>();
options.Interceptors.Add<ClinicalAuditInterceptor>();
});
// Or per-service
builder.Services.AddGrpc();
builder.Services.AddGrpcServiceOptions<ClinicalPatientGrpcService>(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});
app.MapGrpcService<ClinicalPatientGrpcService>();Client-Side Retry Interceptor
public sealed class RetryInterceptor : Interceptor
{
private const int MaxRetries = 3;
private const int DelayMs = 1000;
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
return new AsyncUnaryCall<TResponse>(
RetryAsync(request, context, continuation),
Task.FromResult(Metadata.Empty),
() => new Status(),
() => Metadata.Empty,
() => { });
}
private static async Task<TResponse> RetryAsync<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
where TRequest : class
where TResponse : class
{
for (int attempt = 0; attempt < MaxRetries; attempt++)
{
try
{
return await continuation(request, context).ResponseAsync;
}
catch (RpcException ex) when (
attempt < MaxRetries - 1 &&
(ex.StatusCode == StatusCode.Unavailable ||
ex.StatusCode == StatusCode.DeadlineExceeded))
{
await Task.Delay(DelayMs * (attempt + 1)); // exponential backoff
}
}
throw new RpcException(new Status(StatusCode.Unavailable, "Max retries exceeded."));
}
}
// Register on the client channel
var channel = GrpcChannel.ForAddress("https://api.systemforge.internal");
var invoker = channel.Intercept(new RetryInterceptor());
var client = new ClinicalPatientService.ClinicalPatientServiceClient(invoker);AddGrpcClient with Interceptors
// DI-registered gRPC client with interceptors
builder.Services.AddGrpcClient<ClinicalPatientService.ClinicalPatientServiceClient>(options =>
{
options.Address = new Uri("https://clinical-api.internal");
})
.AddInterceptor<RetryInterceptor>()
.AddInterceptor<AuthHeaderInterceptor>(); // attach JWT to outgoing calls
public sealed class AuthHeaderInterceptor : Interceptor
{
private readonly ICurrentUser _currentUser;
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var headers = context.Options.Headers ?? new Metadata();
headers.Add("authorization", $"Bearer {_currentUser.AccessToken}");
var newOptions = context.Options.WithHeaders(headers);
var newContext = new ClientInterceptorContext<TRequest, TResponse>(
context.Method, context.Host, newOptions);
return continuation(request, newContext);
}
}Production issue I've seen: A gRPC client service called a downstream service with no retry logic. A transient network hiccup during a 3-second window caused 400 drug order status checks to fail simultaneously. All 400 errors surfaced as permanent failures in the pharmacy system's dashboard. A retry interceptor with exponential backoff and
Unavailablestatus code handling would have recovered automatically.
Key Takeaway
gRPC interceptors wrap calls on server and client sides for cross-cutting concerns: logging, error handling, auditing, retry, authentication. Server interceptors register in
AddGrpc(options.Interceptors.Add<T>()). Client interceptors register withchannel.Intercept()orAddGrpcClient().AddInterceptor<T>(). Always catchExceptionin server interceptors and wrap inRpcExceptionwith a safe message — raw exceptions expose internal details to clients.
gRPC Knowledge Check
5 questions · Test what you just learned · Instant explanations
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.