首页 > 解决方案 > 如何将 gRPC C# 服务器错误拦截器中捕获的异常发送到 TypeScript gRPC-Web 客户端?

问题描述

我需要向客户端发送自定义异常消息。我有以下代码:

services.AddGrpc(options => options.Interceptors.Add<ErrorInterceptor>());
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
{
    try
    {
        return await continuation(request, context);
    }
    catch (ValidationException validationExc)
    {
        await WriteResponseHeadersAsync(StatusCode.InvalidArgument, translation =>
             translation.GetEnumTranslation(validationExc.Error, validationExc.Parameters));
    }
    catch (Exception)
    {
        await WriteResponseHeadersAsync(StatusCode.Internal, translation =>
             translation.GetEnumTranslation(HttpStatusCode.InternalServerError));
    }
    return default;

    Task WriteResponseHeadersAsync(StatusCode statusCode, Func<ITranslationService, string> getMessage)
    {
        var httpContext = context.GetHttpContext();
        var translationService = httpContext.RequestServices.GetService<ITranslationService>();
        var errorMessage = getMessage(translationService);
        var responseHeaders = new Metadata
        {
            { nameof(errorMessage) , errorMessage },//1) can see in browser's devTools, but not in the code
            { "content-type" , errorMessage },//2) ugly, but works
        };
        context.Status = new Status(statusCode, errorMessage);//3) not working
        return context.WriteResponseHeadersAsync(responseHeaders);//4) alternative?
    }
}
    this.grpcClient.add(request, (error, reply: MaskInfoReply) => {
        this.grpcBaseService.handleResponse<MaskInfoReply.AsObject>(error, reply, response => {
            const mask = new Mask(response.id, response.name);
            callback(mask);
        });
    });
    handleResponse<T>(error: ServiceError,
        reply: {
            toObject(includeInstance?: boolean): T;
        },
        func: (response: T) => void) {

        if (error) {
            const errorMessage = error.metadata.headersMap['content-type'][0];
            this.toasterService.openSnackBar(errorMessage, "Ok");
            console.error(error);
            return;
        }
        const response = reply.toObject();
        func(response);
    }
  1. 我想使用 Status 发送错误(评论 3),但它没有改变
  2. 我想知道是否有另一种方法可以不在响应标头中发送它(评论 4)
  3. 我尝试添加自定义响应标头(评论 1),但我在客户端代码中收到的唯一一个是“内容类型”,所以我决定覆盖它(评论 2)

标签: c#asp.nettypescript.net-coregrpc

解决方案


我最近遇到了同样的死胡同,并决定这样做:

  1. 创建错误模型:

    message ValidationErrorDto {
        // A path leading to a field in the request body.
        string field = 1;
    
        // A description of why the request element is bad.
        string description = 2;
    }
    
    message ErrorSynopsisDto {
      string traceTag = 1;
      repeated ValidationErrorDto validationErrors = 2;
    }
    
  2. 为将对象序列化为 JSON 的错误模型创建扩展:

    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    
    public static class ErrorSynopsisDtoExtension
    {
        public static string ToJson(this ErrorSynopsisDto errorSynopsisDto) =>
        JsonConvert.SerializeObject(
            errorSynopsisDto,
            new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            });
    }
    
  3. 创建一个封装错误模型的自定义异常:

    public class OperationException : Exception
    {
        private readonly List<ValidationErrorDto> validationErrors = new();
        public bool HasValidationErrors => this.validationErrors.Count > 0;
    
        public OperationException(string traceTag) : base
            (
                new ErrorSynopsisDto
                {
                    TraceTag = traceTag
                }.ToJson() // <- here goes that extension
            ) => ErrorTag = traceTag;
    
        public OperationException(
            string traceTag,
            List<ValidationErrorDto> validationErrors
        ) : base
            (
                new ErrorSynopsisDto
                {
                    TraceTag = traceTag,
                    ValidationErrors = { validationErrors }
                }.ToJson() // <- here goes that extension again
            )
        {
            ErrorTag = traceTag;
            this.validationErrors = validationErrors;
        }
    }
    
  4. 从服务调用处理程序抛出自定义异常:

    throw new OperationException(
        "MY_CUSTOM_VALIDATION_ERROR_CODE",
        // the following block can be simplified with a mapper, for reduced boilerplate
        new()
        {
            new()
            {
                Field = "Profile.FirstName",
                Description = "Is Required."
            }
        }
    );
    
  5. 最后,异常拦截器:

    public class ExceptionInterceptor : Interceptor
    {
        private readonly ILogger<ExceptionInterceptor> logger;
    
        public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger) => this.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 (OperationException ex)
            {
                this.logger.LogError(ex, context.Method);
                var httpContext = context.GetHttpContext();
    
                if (ex.HasValidationErrors)
                {
                    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                }
                else
                {
                    httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
                }
    
                throw;
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, context.Method);
                var httpContext = context.GetHttpContext();
                httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
                var opEx = new OperationException("MY_CUSTOM_INTERNAL_ERROR_CODE");
    
                throw new RpcException(
                    new Status(
                        StatusCode.Internal,
                        opEx.Message
                    )
                );
    
            }
        }
    }
    

在基于 TypeScript 的前端,我只是捕获 RPC 错误并像这样对消息进行水合:

JSON.parse(err.message ?? {}) as ErrorSynopsisDto

推荐阅读