.NET 8 gRPC如何捕获客户端断开引发的TaskCanceledException
ASP.NET Core gRPC 客户端断开异常捕获问题
问题背景
我用.NET 8构建ASP.NET Core gRPC服务,将MyGrpcService注册为Singleton。当客户端在长请求过程中突然断开连接时,日志会抛出以下异常:
Error when executing service method 'MyGrpcMethod'.|"System.Threading.Tasks.TaskCanceledException: The request was aborted ---> Microsoft.AspNetCore.Connections.ConnectionAbortedException: The HTTP/2 stream was reset by the application with error code CANCEL. at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result) at System.IO.Pipelines.Pipe.GetReadAsyncResult() at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2MessageBody.ReadAsync(CancellationToken cancellationToken) --- End of inner exception stack trace --- at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2MessageBody.ReadAsync(CancellationToken cancellationToken) at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token) at Grpc.AspNetCore.Server.Internal.PipeExtensions.ReadSingleMessageAsync[T](PipeReader input, HttpContextServerCallContext serverCallContext, Func`2 deserializer) at Grpc.AspNetCore.Server.Internal.CallHandlers.UnaryServerCallHandler`3.HandleCallAsyncCore(HttpContext httpContext, HttpContextServerCallContext serverCallContext) at Grpc.AspNetCore.Server.Internal.CallHandlers.ServerCallHandlerBase`3.<HandleCallAsync>g__AwaitHandleCall|8_0(HttpContextServerCallContext serverCallContext, Method`2 method, Task handleCall)
但配置的ExceptionHandlingInterceptor拦截器和GrpcExceptionMiddleware中间件都无法捕获该异常。我尝试在长耗时gRPC方法中通过CancellationToken.Register/UnsafeRegister监听客户端断开,但相关日志没触发,外层try-catch也抓不到异常。
环境信息
-.NET 8
-ASP.NET Core gRPC服务
-gRPC服务注册为Singleton
-使用拦截器和中间件处理异常
-客户端可能在长请求过程中断开连接
简化代码
异常拦截器
public class ExceptionHandlingInterceptor : Interceptor { private readonly ILogger<ExceptionHandlingInterceptor> _logger; public ExceptionHandlingInterceptor(ILogger<ExceptionHandlingInterceptor> logger) { _logger = logger; } public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation) { try { return await continuation(request, context); } catch (Exception ex) when (context.CancellationToken.IsCancellationRequested) { _logger.LogError(ex, "gRPC request was aborted"); throw; } catch (Exception ex) { _logger.LogError(ex, "Unhandled gRPC exception"); throw; } } }
gRPC异常中间件
public class GrpcExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger<GrpcExceptionMiddleware> _logger; public GrpcExceptionMiddleware(RequestDelegate next, ILogger<GrpcExceptionMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var isGrpc = context.Request.ContentType?.StartsWith("application/grpc") == true; try { await _next(context); } catch (TaskCanceledException ex) when (isGrpc) { _logger.LogWarning(ex, "[gRPC Cancelled] Path={Path}", context.Request.Path); throw; } catch (ConnectionAbortedException ex) when (isGrpc) { _logger.LogWarning(ex, "[gRPC Aborted] Path={Path}", context.Request.Path); throw; } catch (IOException ex) when (isGrpc) { _logger.LogWarning(ex, "[gRPC IO Error] Path={Path}", context.Request.Path); throw; } catch (Exception ex) when (isGrpc) { _logger.LogError(ex, "[gRPC Unknown Error] Path={Path}", context.Request.Path); throw; } } }
服务方法示例
public override async Task<ResponseModel> MyGrpcMethod(RequestModel request, ServerCallContext context) { try { var ct = context.CancellationToken; // 尝试监听客户端断开 ct.UnsafeRegister(state => { var logger = (ILogger)state; logger.LogError("Client disconnected (UnsafeRegister)"); }, MyLogger); ct.Register(() => { MyLogger.LogError("Client disconnected (Register)"); }); // 模拟异步工作 var taskId = await MyProcessor.EnqueueTask(async () => { await Task.Delay(TimeSpan.FromSeconds(10), ct); // 模拟长任务 return await MyService.DoWorkAsync(request, ct); }); var result = await MyProcessor.WaitForCompletionAsync(taskId, ct); return new ResponseModel { Code = 0, Message = "Success" }; } catch (Exception ex) { MyLogger.LogError(ex, "Error in MyGrpcMethod"); return new ResponseModel { Code = -1, Message = ex.Message }; } }
问题
- 如何在HTTP/2上的gRPC中正确捕获并记录客户端断开引发的异常?
- 该异常是否在底层(如管道读取器)抛出,导致自定义拦截器或中间件无法触及?
- 有哪些最佳实践可确保此类场景被记录并可追踪?
解答
1. 正确捕获并记录客户端断开异常的方法
要捕获这类底层异常,需要调整拦截器和中间件的位置,同时利用gRPC的生命周期事件:
- 调整中间件注册顺序:确保
GrpcExceptionMiddleware在UseRouting和UseGrpc之前注册,因为gRPC的请求处理在路由之后,提前注册才能覆盖底层管道的异常。 - 修改拦截器逻辑:拦截器的
UnaryServerHandler仅覆盖业务方法执行阶段,客户端断开异常可能发生在请求读取阶段,需提前监听取消信号并完善异常捕获:public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation) { var cancellationRegistration = context.CancellationToken.Register(() => { _logger.LogWarning("Client disconnected during request execution: {Method}", context.Method); }); try { return await continuation(request, context); } catch (TaskCanceledException ex) when (context.CancellationToken.IsCancellationRequested) { _logger.LogWarning(ex, "gRPC request cancelled by client: {Method}", context.Method); throw new RpcException(new Status(StatusCode.Cancelled, "Client disconnected")); } catch (ConnectionAbortedException ex) { _logger.LogWarning(ex, "gRPC connection aborted by client: {Method}", context.Method); throw new RpcException(new Status(StatusCode.Aborted, "Connection aborted")); } finally { cancellationRegistration.Dispose(); // 释放注册避免内存泄漏 } } - 启用Kestrel底层日志:在
appsettings.json中开启Kestrel的警告级日志,直接捕获HTTP/2连接异常:{ "Logging": { "LogLevel": { "Microsoft.AspNetCore.Server.Kestrel": "Warning", "Microsoft.AspNetCore.Server.Kestrel.Core": "Warning" } } }
2. 异常无法被拦截/中间件捕获的原因
是的,这个异常确实在底层管道读取阶段抛出:
- 从栈追踪可以看到,异常发生在
Http2MessageBody.ReadAsync,也就是gRPC框架读取请求消息的阶段,这个阶段早于业务方法(及拦截器continuation调用)执行。 - 拦截器的
UnaryServerHandler仅包裹业务方法执行,若中间件注册在UseGrpc之后,也无法触及gRPC框架内部的管道处理逻辑,因此抓不到该阶段的异常。
3. 最佳实践
- 使用gRPC原生状态码:捕获异常后转换为
RpcException,返回对应标准状态码(如StatusCode.Cancelled、StatusCode.Aborted),让客户端正确识别错误类型。 - 正确管理CancellationToken注册:在业务方法中注册取消回调后,务必在
finally块释放注册,避免单例服务内存泄漏。 - 实现全局异常处理:通过
IServerErrorHandler接口捕获gRPC框架全阶段异常,包括底层管道错误:
在public class GrpcGlobalErrorHandler : IServerErrorHandler { private readonly ILogger<GrpcGlobalErrorHandler> _logger; public GrpcGlobalErrorHandler(ILogger<GrpcGlobalErrorHandler> logger) { _logger = logger; } public bool HandleException(Exception exception, out Status status) { if (exception is TaskCanceledException || exception is ConnectionAbortedException) { _logger.LogWarning(exception, "gRPC request cancelled/aborted"); status = new Status(StatusCode.Cancelled, "Client disconnected"); return true; } _logger.LogError(exception, "Unhandled gRPC error"); status = new Status(StatusCode.Internal, "Server error"); return true; } }Program.cs注册:builder.Services.AddGrpc(options => { options.ErrorHandler = new GrpcGlobalErrorHandler(builder.Services.GetRequiredService<ILogger<GrpcGlobalErrorHandler>>()); }); - 结构化日志:记录异常时包含请求路径、方法名、客户端IP等信息,便于问题追踪。
- 单例服务避免持有请求上下文:单例生命周期长于请求,不要保存
ServerCallContext或CancellationToken,防止上下文泄漏。
内容的提问来源于stack exchange,提问作者Po-Sen
相关产品推荐
相关产品推荐

