You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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 };
    }
}

问题

  1. 如何在HTTP/2上的gRPC中正确捕获并记录客户端断开引发的异常?
  2. 该异常是否在底层(如管道读取器)抛出,导致自定义拦截器或中间件无法触及?
  3. 有哪些最佳实践可确保此类场景被记录并可追踪?

解答

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 17:30:53