c# - 如何将 gRPC C# 服务器错误拦截器中捕获的异常发送到 TypeScript gRPC-Web 客户端?
问题描述
我需要向客户端发送自定义异常消息。我有以下代码:
- 在 Startup.cs 的 ConfigureServices 方法中
services.AddGrpc(options => options.Interceptors.Add<ErrorInterceptor>());
- 在 ErrorInterceptor.cs
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?
}
}
- 在掩码-http.service.ts
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);
});
});
- 在 grpc-base.service.ts
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);
}
- 我想使用 Status 发送错误(评论 3),但它没有改变
- 我想知道是否有另一种方法可以不在响应标头中发送它(评论 4)
- 我尝试添加自定义响应标头(评论 1),但我在客户端代码中收到的唯一一个是“内容类型”,所以我决定覆盖它(评论 2)
解决方案
我最近遇到了同样的死胡同,并决定这样做:
创建错误模型:
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; }
为将对象序列化为 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() }); }
创建一个封装错误模型的自定义异常:
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; } }
从服务调用处理程序抛出自定义异常:
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." } } );
最后,异常拦截器:
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
推荐阅读
- bootstrap-4 - 是否可以在标题属性(data-html =“true”)Bootstrap 4 Tooltip的标签内添加类?
- javascript - Puppeteer - 如何使用 XPath 获取标签的长度?
- sql - 如何使用 Select Query 更新前 (20) 条记录
- javascript - 如何运行我 git 克隆的程序?
- laravel - Http 异常 Laravel 5.6
- linux - expr:Cygwin 中的非整数参数错误
- sql - PostgreSQL 是否保证两个同时提交的“提交时间戳”的唯一性?
- javascript - 在另一个不工作的内部返回一个承诺
- json - 以逗号分隔的字符串 JSON 中的转义引号
- sql - 从现有查询返回结果的过程