ASP.NET Core Web API异常处理


280

在使用常规ASP.NET Web API多年之后,我将ASP.NET Core用于新的REST API项目。我看不到任何在ASP.NET Core Web API中处理异常的好方法。我试图实现异常处理过滤器/属性:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

这是我的启动过滤器注册:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

我遇到的问题是,当发生异常时,AuthorizationFilter它不会由处理ErrorHandlingFilter。我期望它会像在旧的ASP.NET Web API中一样被捕获。

那么,如何捕获所有应用程序异常以及动作过滤器中的任何异常?


3
您是否尝试过UseExceptionHandler中间件?
Pawel

我在这里有一个有关如何使用UseExceptionHandler中间件的示例
Ilya Chernomordik

Answers:


538

异常处理中间件

在使用不同的异常处理方法进行了许多实验之后,我最终使用了中间件。对于我的ASP.NET Core Web API应用程序,它表现得最好。它可以处理应用程序异常以及动作过滤器中的异常,并且我可以完全控制异常处理和HTTP响应。这是我的异常处理中间件:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

在课堂上在MVC之前注册它Startup

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

您可以添加堆栈跟踪,异常类型名称,错误代码或您想要的任何内容。非常灵活。这是异常响应的示例:

{ "error": "Authentication token is not valid." }

当序列化响应对象以利用ASP.NET MVC的序列化设置在所有端点之间实现更好的序列化一致性时,请考虑注入IOptions<MvcJsonOptions>Invoke方法,然后使用该方法JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)

方法2

UseExceptionHandler对于简单的方案,还有一个非显而易见的API 可以正常工作:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

这不是设置异常处理的非常明显但简单的方法。但是,我仍然更喜欢中间件方法,因为我获得了更多控制权并具有注入必要依赖项的能力。


4
我一直在努力争取使定制的中间件今天能够正常工作,但它的工作方式基本上相同(我正在使用它来管理请求的工作单元/事务)。我面临的问题是“ next”中引发的异常未在中间件中捕获。可以想象,这是有问题的。我在做什么错/想念?有任何指示或建议吗?
brappleye3 '17

5
@ brappleye3-我想出了问题所在。我只是在Startup.cs类的错误位置注册了中间件。我搬到app.UseMiddleware<ErrorHandlingMiddleware>();了之前app.UseStaticFiles();。现在似乎已正确捕获了异常。这使我相信app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();做一些内部魔术中间件黑客可以正确地订购中间件。
贾马丹'17

4
我同意自定义中间件可能非常有用,但会在NotFound,Unauthorized和BadRequest情况下使用异常产生疑问。为什么不简单地设置状态代码(使用NotFound()等),然后在您的自定义中间件中或通过UseStatusCodePagesWithReExecute处理它?有关更多信息,请参见devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api
Paul Hiles

4
这很糟糕,因为它总是序列化为JSON,而完全忽略了内容协商。
康拉德

5
@Konrad有效点。这就是为什么我说这个示例是您开始的地方,而不是最终结果。对于99%的API,JSON绰绰有余。如果您觉得这个答案不够好,请随时做出贡献。
Andrei

60

最新的Asp.Net Core(至少从2.2起,可能是更早的版本)具有内置的中间件,与接受的答案中的实现相比,它稍微容易一些:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

它应该做的差不多,只需要编写更少的代码。

重要:请记住在顺序之前UseMvc(或UseRouting在.Net Core 3中)添加它。


它是否支持将DI作为处理程序的参数,还是必须在处理程序中使用服务定位器模式?
lp

32

最好的选择是使用中间件来实现所需的日志记录。您希望将异常日志记录放在一个中间件中,然后处理在另一中间件中向用户显示的错误页面。这样可以分离逻辑并遵循Microsoft对2个中间件组件进行的设计。这是指向Microsoft文档的很好的链接: ASP.Net Core中的错误处理

对于您的特定示例,您可能需要使用StatusCodePage中间件中的扩展之一。或者像这样滚动自己。

您可以在此处找到记录异常的示例:ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

如果您不喜欢该特定实现,则还可以使用ELM中间件,下面是一些示例:Elm Exception中间件

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

如果这不能满足您的需求,则始终可以通过查看ExceptionHandlerMiddleware和ElmMiddleware的实现来滚动自己的Middleware组件,以掌握用于构建自己的Middleware的概念。

重要的是,将异常处理中间件添加到StatusCodePages中间件之下,但要放在所有其他中间件组件之上。这样,您的Exception中间件将捕获异常,将其记录下来,然后允许请求继续进行到StatusCodePage中间件,该中间件将向用户显示友好的错误页面。


别客气。我还提供了一个示例的链接,该示例用于在可能会更好地满足您要求的边缘情况下覆盖默认的UseStatusPages。
阿什莉·李

1
请注意,Elm不会保留日志,建议使用Serilog或NLog提供序列化。参见 ELM日志消失。我们可以将其持久化到文件或数据库吗?
Michael Freidgeim

2
现在链接已断开。
Mathias Lykkegaard Lorenzen

@AshleyLee,我怀疑这UseStatusCodePages在Web API服务实现中有用。根本没有视图或HTML,只有JSON响应...
Paul Michalik

23

公认的答案对我有很大帮助,但我想在中间件中传递HttpStatusCode以便在运行时管理错误状态代码。

根据这个链接,我有一些想法可以做到这一点。因此,我将Andrei答案与此合并。所以我的最终代码如下:
1.基类

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2.自定义异常类类型

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3.自定义异常中间件

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4.扩展方法

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5.在startup.cs中配置方法

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

现在我在Account controller中的登录方法:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

在上面可以看到我是否还没有找到用户,然后引发HttpStatusCodeException,其中我已经传递了HttpStatusCode.NotFound状态和自定义消息
在中间件中

抓(HttpStatusCodeException ex)

被阻止的将被调用,它将控制权传递给

私有Task HandleExceptionAsync(HttpContext上下文,HttpStatusCodeException异常)方法




但是,如果我之前遇到运行时错误怎么办?为此,我使用了try catch块来抛出异常,并将其捕获在catch(Exception exceptionObj)块中,并将控制权传递给

任务HandleExceptionAsync(HttpContext上下文,Exception异常)

方法。

我使用了一个ErrorDetails类来保证一致性。


扩展方法放在哪里?不幸的是startup.csvoid Configure(IapplicationBuilder app)我遇到了一个错误IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware。我添加了参考,在哪里CustomExceptionMiddleware.cs
Spedo De La Rossa

您不希望使用异常,因为它们会使您的api变慢。异常非常昂贵。
lnaie

@Inaie,不能这么说...但是看来您从来没有遇到任何例外。.出色的工作
Arjun

19

要配置每种异常类型的异常处理行为,您可以使用NuGet包中的中间件:

代码示例:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

16

首先,感谢安德烈(Andrei),因为我的解决方案基于他的例子。

我包括我的资料是因为它是一个更完整的示例,可能会节省读者一些时间。

Andrei方法的局限性在于它不处理日志记录,捕获潜在有用的请求变量和内容协商(无论客户端请求什么,XML或纯文本等都将始终返回JSON)。

我的方法是使用ObjectResult,它使我们能够使用MVC中包含的功能。

此代码还可以防止响应缓存。

错误响应已经过修饰,可以由XML序列化程序序列化。

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}

9

首先,将ASP.NET Core 2配置Startup为针对来自Web服务器的任何错误和任何未处理的异常重新执行到错误页面。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

接下来,定义一个异常类型,该异常类型将使您使用HTTP状态代码引发错误。

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

最后,在错误页面的控制器中,根据错误原因以及最终用户是否可以直接看到响应来自定义响应。此代码假定所有API URL均以开头/api/

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core将记录错误详细信息以供您调试,因此状态代码可能就是您想要提供给(可能不受信任的)请求者的全部内容。如果您想显示更多信息,则可以对其进行增强HttpException。对于API错误,您可以通过替换return StatusCode...为来将JSON编码的错误信息放入消息正文中return Json...


0

使用中间件或IExceptionHandlerPathFeature很好。还有另一种方式eshop中

创建一个异常过滤器并注册

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.