ABP异常处理

前言

在ABP中使用UserFriendlyException抛出异常,HTTP状态码为什么是403?

下面用这一段测试代码:[HttpPost]
public async Task<PeopleDto> GetPeopleAsync(string sn)
{
    //这里只是测试abp的UserFriendlyException异常
    if (sn.Equals("123", StringComparison.OrdinalIgnoreCase))
    {
        throw new UserFriendlyException("测试异常");
    }

    if (sn.IsNullOrWhiteSpace())
    {
        throw new UserFriendlyException("查询单号不能为空");
    }

    IQueryable<People> query = await _peopleRepository.GetQueryableAsync();
    query = query.WhereIf(!sn.IsNullOrWhiteSpace(), p => p.SN.Contains(sn));  //这里只是单个条件,真实环境有多个条件
    var item = await ObjectMapper.GetMapper().ProjectTo<PeopleDto>(query.AsNoTracking()).FirstOrDefaultAsync();
    if (item == null)
    {
        throw new UserFriendlyException($"没有查询到单号:{sn}数据");
    }
    return item;
}

在Swagger调用接口:

在ABP使用UserFriendlyException抛出异常,为什么http的状态码是403呢?

返回的Http状态码是403,有时候容易觉得是是在没权限调用这个接口.其实并不是.下面学习一下ABP的异常处理.

ABP是在什么时候处理异常的呢?

ABP是在什么时候处理异常的呢?带着这个疑问,在项目中没看到直接处理异常呢(在Asp.Net Core项目,是使用了很多中间件的)? 我们先看一下启动项目中的OnApplicationInitialization有没有Execption相关的中间件.
ABP项目启动OnApplicationInitialization源码
所以只能查看项目的Main代码:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;

namespace BookStore.Web;

public class Program
{
    public async static Task<int> Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
#if DEBUG
            .MinimumLevel.Information()
#else
            .MinimumLevel.Information()
#endif
            .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
            .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Information)
            .Enrich.FromLogContext()
            .WriteTo.Async(c => c.File("Logs/logs.txt"))
            .WriteTo.Async(c => c.Console())
            .CreateLogger();

        try
        {
            Log.Information("Starting web host.");
            var builder = WebApplication.CreateBuilder(args);
            builder.Host.AddAppSettingsSecretsJson()
                .UseAutofac()
                .UseSerilog();
            await builder.AddApplicationAsync<BookStoreWebModule>();
            var app = builder.Build();
            await app.InitializeApplicationAsync();  //看来看去,是不是在这里
            await app.RunAsync();
            return 0;
        }
        catch (Exception ex)
        {
            if (ex is HostAbortedException)
            {
                throw;
            }

            Log.Fatal(ex, "Host terminated unexpectedly!");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

果然在这里,在AbpApplicationBuilderExtensions.cs看到UseAbpExcptionHandling.

//看到有异常处理的中间件
public static IApplicationBuilder UseAbpExceptionHandling(this IApplicationBuilder app)
{
    if (app.Properties.ContainsKey(ExceptionHandlingMiddlewareMarker))
    {
        return app;
    }

    app.Properties[ExceptionHandlingMiddlewareMarker] = true;
    return app.UseMiddleware<AbpExceptionHandlingMiddleware>();  //看来看去,是不是在这里
}

接着我们继续学习AbpExceptionHandingMiddleware中间件的源码:

public class AbpExceptionHandlingMiddleware : AbpMiddlewareBase, ITransientDependency
{
    private readonly ILogger<AbpExceptionHandlingMiddleware> _logger;

    private readonly Func<object, Task> _clearCacheHeadersDelegate;

    public AbpExceptionHandlingMiddleware(ILogger<AbpExceptionHandlingMiddleware> logger)
    {
        _logger = logger;

        _clearCacheHeadersDelegate = ClearCacheHeaders;
    }

    public async override Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            //***如果没有异常,处理下一个中间件
            await next(context);
        }
        catch (Exception ex)
        {
            //***有异常时,捕获异常
            // We can't do anything if the response has already started, just abort.
            if (context.Response.HasStarted)
            {
                _logger.LogWarning("An exception occurred, but response has already started!");
                throw;
            }

            if (context.Items["_AbpActionInfo"] is AbpActionInfoInHttpContext actionInfo)
            {
                if (actionInfo.IsObjectResult) //TODO: Align with AbpExceptionFilter.ShouldHandleException!
                {
                    //具体处理异常
                    await HandleAndWrapException(context, ex);
                    return;
                }
            }

            throw;
        }
    }

    private async Task HandleAndWrapException(HttpContext httpContext, Exception exception)
    {
        _logger.LogException(exception);

        await httpContext
            .RequestServices
            .GetRequiredService<IExceptionNotifier>()
            .NotifyAsync(
                new ExceptionNotificationContext(exception)
            );

        if (exception is AbpAuthorizationException)
        {
            await httpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
                .HandleAsync(exception.As<AbpAuthorizationException>(), httpContext);
        }
        else
        {
            var errorInfoConverter = httpContext.RequestServices.GetRequiredService<IExceptionToErrorInfoConverter>();
            var statusCodeFinder = httpContext.RequestServices.GetRequiredService<IHttpExceptionStatusCodeFinder>(); //***看到Http Exception StatusCode
            var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>();
            var exceptionHandlingOptions = httpContext.RequestServices.GetRequiredService<IOptions<AbpExceptionHandlingOptions>>().Value;

            httpContext.Response.Clear();
            httpContext.Response.StatusCode = (int)statusCodeFinder.GetStatusCode(httpContext, exception);
            httpContext.Response.OnStarting(_clearCacheHeadersDelegate, httpContext.Response);
            httpContext.Response.Headers.Append(AbpHttpConsts.AbpErrorFormat, "true");
            httpContext.Response.Headers.Append("Content-Type", "application/json");

            await httpContext.Response.WriteAsync(
                jsonSerializer.Serialize(
                    new RemoteServiceErrorResponse(
                        errorInfoConverter.Convert(exception, options =>
                        {
                            options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;
                            options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;
                        })
                    )
                )
            );
        }
    }

    private Task ClearCacheHeaders(object state)
    {
        var response = (HttpResponse)state;

        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);

        return Task.CompletedTask;
    }
}

再接着我们看

public class DefaultHttpExceptionStatusCodeFinder : IHttpExceptionStatusCodeFinder, ITransientDependency
{
    protected AbpExceptionHttpStatusCodeOptions Options { get; }

    public DefaultHttpExceptionStatusCodeFinder(
        IOptions<AbpExceptionHttpStatusCodeOptions> options)
    {
        Options = options.Value;
    }

    public virtual HttpStatusCode GetStatusCode(HttpContext httpContext, Exception exception)
    {
        if (exception is IHasHttpStatusCode exceptionWithHttpStatusCode &&
            exceptionWithHttpStatusCode.HttpStatusCode > 0)
        {
            return (HttpStatusCode)exceptionWithHttpStatusCode.HttpStatusCode;
        }

        if (exception is IHasErrorCode exceptionWithErrorCode &&
            !exceptionWithErrorCode.Code.IsNullOrWhiteSpace())
        {
            if (Options.ErrorCodeToHttpStatusCodeMappings.TryGetValue(exceptionWithErrorCode.Code!, out var status))
            {
                return status;
            }
        }

        if (exception is AbpAuthorizationException)
        {
            return httpContext.User.Identity!.IsAuthenticated
                ? HttpStatusCode.Forbidden
                : HttpStatusCode.Unauthorized;
        }

        //TODO: Handle SecurityException..?

        if (exception is AbpValidationException)
        {
            return HttpStatusCode.BadRequest;  //400
        }

        if (exception is EntityNotFoundException)
        {
            return HttpStatusCode.NotFound;  //4040
        }

        if (exception is AbpDbConcurrencyException)
        {
            return HttpStatusCode.Conflict;  //409
        }

        if (exception is NotImplementedException)
        {
            return HttpStatusCode.NotImplemented; //501
        }

        if (exception is IBusinessException)
        {
            return HttpStatusCode.Forbidden;  //403 符合我们使用UserFriendlyException响应的状态码
        }

        return HttpStatusCode.InternalServerError; //500
    }
}

看到403状态码对应的异常,是IBusinessException,我们在回到UserFriendlyException,对UserFriendlyException源码进行查看.

//继承BusinessException,实现IUserFriendException
[Serializable]
public class UserFriendlyException : BusinessException, IUserFriendlyException
{
}
//IUserFriendlyException 实现IBusinessException接口
public interface IUserFriendlyException : IBusinessException
{
}

到这里基本上,弄明白UserFriendlyException是如何返回403状态码的.怎么对UseAbpExceptionHandling启用呢?,查找源码发现是在启用工作单元之前,先启用的UseAbpExceptionHandling.

public static IApplicationBuilder UseUnitOfWork(this IApplicationBuilder app)
{
    return app
        .UseAbpExceptionHandling()  //先启用异常处理的中间件
        .UseMiddleware<AbpUnitOfWorkMiddleware>(); //启用工作单元的中间件
}

这样就和Module中OnApplicationInitialization中启用工作单元中间件衔接起来了.

app.UseUnitOfWork(); //使用工作单元中间件


秋风 2024-08-06