手撸 HTTP 请求统一返回格式中间件

1. 概述

中间件就是为了解决实际问题的脚手架,在我们开发微服务过程中,经常会碰到一个问题:不同的微服务有不同的开发人员,在处理 HTTP 返回数据时,格式五花八门,难以统一。

主要问题:

  1. 只返回请求数据、没有错误码;
  2. 返回的错误码定义规则五花八门;
  3. 微服务之间的可视化 traceId 不统一或者没有 traceId;
  4. 直接抛异常到 response

1.1 需求分析

针对以上问题,我们可以自己实现一个 HTTP 返回格式中间件,统一规范。我们希望这个中间件能实现以下功能:

  1. API 接口只返回请求数据,不需要自己定义 error_code、error_message
  2. 自动捕获 API 返回结果,根据返回结果封装统一的返回格式,添加 error_code、error_message
  3. 根据错误码定义,接口逻辑只需抛出对应错误码的异常,中间件自动捕获异常,封装返回结果
  4. 自动生成 traceId,并返回 traceId,支持可视化观测

1.2 实现方案

针对 1.1 的需求我们可以大概制定实现方案

  1. 制定接口返回规范文档。

    兵马未动,粮草先行,一个合理的规范文档是方案的关键。定义了 code、message、traceId 三个关键元素,并根据 code 决定是否返回 data

    image

  2. 根据 code 状态码定义异常类型

    2.1 定义异常状态码

    public static class AIoTDomainErrorCodes
    {
        /* You can add your business exception error codes here, as constants */
        public static int DeviceOffline = 50001;
        public static int DeviceNotFound = 50003;
        public static int RequestNotExecuted = 50006;
        public static int HwHttpApiException = 50007;
    }
    

    2.2 实现异常类型(以 DeviceNotFound 为例)

    public class DeviceNotFoundException : AbpException, ICustomException
        {
            public int ErrorCode { get; set; } = AIoTDomainErrorCodes.DeviceNotFound;
            public DeviceNotFoundException()
            {
            }
    
            public DeviceNotFoundException(string? message)
                : base(message)
            {
            }
    
            public DeviceNotFoundException(string? message, Exception? innerException)
                : base(message, innerException)
            {
            }
    
            public DeviceNotFoundException(SerializationInfo serializationInfo, StreamingContext context)
                : base(serializationInfo, context)
            {
            }
        }
    
  3. 定义 ApiResponse 中间件 ApiResponseMiddleware

    使用 ABP 框架 DOTNET CORE 管道配置手柄 IApplicationBuilder,配置自定义的中间件管道。在 ApiResponse 处理 HttpContext 的返回数据,这部分都是处理无异常返回的,通过解析返回数据 Body 来添加对应 code,message,traceId。步骤如下:

    3.1 定义中间件管道 ApiResponseMiddleware

    namespace MeiYiJia.AIoT.Middlewares
    {
        public class ApiResponseMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly IJsonSerializer _jsonSerializer;
            private readonly ILogger<ApiResponseMiddleware> _logger;
    
            public ApiResponseMiddleware(RequestDelegate next, IJsonSerializer jsonSerializer, ILogger<ApiResponseMiddleware> logger)
            {
                _next = next;
                _jsonSerializer = jsonSerializer;
                _logger = logger;
            }
    
            public async Task InvokeAsync(HttpContext context)
            {
                var path = context.Request.Path.ToString().ToLower();
                if ((path.Contains("swagger")
                    //|| context.Request.Path.ToString().ToLower().Contains("login")
                    || path.Contains("monitor")
                    || path.Contains("cap")
                    || path.Contains("hangfire")
                    || path == "/"
                    || path.Contains("/remote")
                    || path.Contains("/abp/")
                    || path.Contains("excel")
                  ))
                {
                    await _next(context);
                    return;
                }
    
                context.Request.EnableBuffering();
                var originalBody = context.Response.Body;
    
                try
                {
                    // 统一处理返回结果 
                    if (path.Contains("/api/") && (context.Response.StatusCode == StatusCodes.Status200OK || context.Response.StatusCode == StatusCodes.Status204NoContent) && IsJsonRequest(context))
                    {
                        using var ms = new MemoryStream();
    
                        context.Response.Body = ms;
    
                        await _next(context);
    
                        if (context.Response.StatusCode == StatusCodes.Status204NoContent)
                            context.Response.OnStarting(state =>
                            {
                                var httpContext = (HttpContext)state;
                                httpContext.Response.StatusCode = StatusCodes.Status200OK;
                                httpContext.Response.ContentType = "application/json";
                                return Task.CompletedTask;
                            }, context);
    
                        var responseStr = GetResponseStr(ms);
    
                        if (!((responseStr?.StartsWith("{\"code\"") ?? false) && (responseStr?.Contains("\"message\"") ?? false) && (responseStr?.Contains("\"data\"") ?? false)))
                        {
                            var isError = StatusCodes.Status400BadRequest <= context.Response.StatusCode && context.Response.StatusCode <= StatusCodes.Status511NetworkAuthenticationRequired;
                            var responseBody = string.IsNullOrEmpty(responseStr) ? null : TryDeserialize(responseStr);
    
                            var traceIdProvider = context.RequestServices.GetRequiredService<ITraceIdProvider>();
                            var traceId = traceIdProvider.Get();
    
                            var apiResult = new ApiResult<dynamic>(isError ? context.Response.StatusCode : 0, isError ? "error" : "success", responseBody, traceId);
                            if (responseStr?.Contains("\"isAsync\":true") ?? false)
                            {
                                apiResult.Code = 100;
                            }
                            var json = _jsonSerializer.Serialize(apiResult);
                            ms.Position = 0;
                            ms.Write(Encoding.UTF8.GetBytes(json));
                        }
    
                        ms.Position = 0;
                        await ms.CopyToAsync(originalBody);
    
                    }
                    // 处理非josn 文件类型
                    else
                    {
                        await _next(context);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError($"统一封装返回结构失败!{ex.Message}", ex);
                }
                finally
                {
                    context.Response.Body = originalBody;
                }
    
            }
    
            private static bool IsJsonRequest(HttpContext context)
            {
                // 获取请求的Content-Type头信息
                var contentType = context.Request.ContentType ?? "application/json";
    
                // 如果Content-Type为application/json,则认为是Json请求
                return contentType != null && contentType.Contains("application/json", StringComparison.OrdinalIgnoreCase);
            }
    
            private static string? GetResponseStr(MemoryStream ms)
            {
                ms.Position = 0;
                var responseStr = new StreamReader(ms).ReadToEnd();
                return responseStr;
            }
    
            private dynamic? TryDeserialize(string? str)
            {
                if (string.IsNullOrWhiteSpace(str))
                    return null;
                try
                {
                    return _jsonSerializer.Deserialize<dynamic>(str);
                }
                catch (Exception)
                {
                    return str;
                }
            }
        }
    }
    
    

    3.2 中间件管道配置扩展

        public static partial class ApplicationBuilderExtensions
        {
            /// <summary>
            /// 统一返回格式
            /// </summary>
            /// <returns></returns>
            public static IApplicationBuilder UseApiResponse(this IApplicationBuilder app)
            {
                return app.UseMiddleware<ApiResponseMiddleware>();
            }
        }
    

    3.3 调用中间件配置

     public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            var app = context.GetApplicationBuilder();
            var env = context.GetEnvironment();
    
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
    
            app.UseAbpRequestLocalization();
    
            app.UseCorrelationId();
            app.UseApiResponse();
            app.UseRequestLog();
    
    
        }
    
  4. 定义 HTTP 异常捕获过滤器 ResultExceptionFilter

ResultExceptionFilter.cs

public class ResultExceptionFilter : IAsyncExceptionFilter, ITransientDependency
{
    public virtual async Task OnExceptionAsync(ExceptionContext context)
    {
        if (!ShouldHandleException(context))
        {
            return;
        }

        await HandleAndWrapException(context);
    }

    protected virtual bool ShouldHandleException(ExceptionContext context)
    {
        //TODO: Create DontWrap attribute to control wrapping..?

        if (context.ActionDescriptor.IsControllerAction() &&
            context.ActionDescriptor.HasObjectResult())
        {
            return true;
        }

        if (context.HttpContext.Request.CanAccept(MimeTypes.Application.Json))
        {
            return true;
        }

        if (context.HttpContext.Request.IsAjax())
        {
            return true;
        }

        return false;
    }

    protected virtual async Task HandleAndWrapException(ExceptionContext context)
    {
        //TODO: Trigger an AbpExceptionHandled event or something like that.

        var exceptionHandlingOptions = context.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;
        var exceptionToErrorInfoConverter = context.GetRequiredService<IExceptionToErrorInfoConverter>();
        var remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, options =>
       {
           options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;
           options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;
       });


        var logLevel = context.Exception.GetLogLevel();

        var remoteServiceErrorInfoBuilder = new StringBuilder();
        remoteServiceErrorInfoBuilder.AppendLine($"---------- {nameof(RemoteServiceErrorInfo)} ----------");
        remoteServiceErrorInfoBuilder.AppendLine(context.GetRequiredService<IJsonSerializer>().Serialize(remoteServiceErrorInfo, indented: true));

        var logger = context.GetService<ILogger<ResultExceptionFilter>>(NullLogger<ResultExceptionFilter>.Instance);

        logger.LogWithLevel(logLevel, remoteServiceErrorInfoBuilder.ToString());

        logger.LogException(context.Exception, logLevel);

        await context.GetRequiredService<IExceptionNotifier>().NotifyAsync(new ExceptionNotificationContext(context.Exception));

        if (context.Exception is AbpAuthorizationException)
        {
            await context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
                .HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext);
        }
        else
        {
            context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
            var statusCode = (int)context
            .GetRequiredService<IHttpExceptionStatusCodeFinder>()
            .GetStatusCode(context.HttpContext, context.Exception);
            context.HttpContext.Response.StatusCode = statusCode;

            var errorCode = 50000;
            if (context.Exception is ICustomException)
            {
                errorCode = (context.Exception as ICustomException)!.ErrorCode;
            }

            var traceIdProvider = context.GetRequiredService<ITraceIdProvider>();
            var traceId = traceIdProvider.Get();
            var result = new ApiResult(errorCode, context.Exception.Message, null, traceId, remoteServiceErrorInfo);

            context.Result = result;
        }

        context.Exception = null; //Handled!
    }
}

OnExceptionAsync​是异常处理重写方法,包含是否需要处理方法 ShouldHandleException​ 和具体处理方法 HandleAndWrapException

主要关注异常的具体处理方法中分为两步:

  1. 授权异常问题处理 context.HttpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>() .HandleAsync(context.Exception.As<AbpAuthorizationException>(), context.HttpContext)
  2. 业务异常问题处理,我们已定义了若干自定义异常继承接口 ICustomException​ 通过 context.Exception is ICustomException​ 就能判断是否是自定义异常,并匹配对应的 ErrorCode,最后再使用 traceIdProvider​ 生成 traceId
  3. 服务依赖注入
    /// <summary>
    /// 异常处理
    /// </summary>
    /// <param name="context"></param>
    private void ConfigureAbpExceptions(ServiceConfigurationContext context)
    {
        context.Services.AddMvc(options => { options.Filters.Add(typeof(ResultExceptionFilter)); });
    }

  • C#
    29 引用 • 34 回帖 • 5 关注
  • HTTP
    75 引用 • 127 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...