ASP.NET Core 웹 API 예외 처리


280

수년간 일반 ASP.NET 웹 API를 사용한 후 새 REST API 프로젝트에 ASP.NET Core를 사용하고 있습니다. 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 웹 API와 같이 작동하는 것처럼 잡힐 것으로 기대했습니다.

그렇다면 액션 필터의 예외뿐만 아니라 모든 응용 프로그램 예외를 어떻게 잡을 수 있습니까?


3
UseExceptionHandler미들웨어 를 사용해 보셨습니까 ?
Pawel

여기UseExceptionHandler 미들웨어 사용법에 대한 예가 있습니다
Ilya Chernomordik

답변:


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간단한 시나리오를 위해 "ok"로 작동하는 명백한 또 다른 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
나는 오늘 사용자 정의 미들웨어를 사용하려고 노력하면서 책상에 대고 머리를 두들 겼으며 기본적으로 동일한 방식으로 작동합니다 (요청에 대한 작업 단위 / 트랜잭션을 관리하는 데 사용하고 있습니다). 내가 직면하고있는 문제는 '다음'에서 발생한 예외가 미들웨어에서 포착되지 않는다는 것입니다. 당신이 상상할 수 있듯이, 이것은 문제가 있습니다. 내가 뭘 잘못하고 / 누락하고 있는가? 어떤 조언이나 제안?
brappleye3

5
@ brappleye3-문제가 무엇인지 알아 냈습니다. 방금 Startup.cs 클래스의 미들웨어를 잘못된 위치에 등록했습니다. 나는 app.UseMiddleware<ErrorHandlingMiddleware>();바로 직전에 이사 했다 app.UseStaticFiles();. 이제 예외가 올바르게 잡힌 것 같습니다. 이것은 app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();미들웨어 주문을 올바르게하기 위해 내부의 마술 미들웨어 해커 를 믿습니다 .
Jamadan

4
사용자 정의 미들웨어가 매우 유용 할 수 있지만 NotFound, Unauthorized 및 BadRequest 상황에 대한 예외를 사용하여 질문하는 것에 동의합니다. NotFound () 등을 사용하여 상태 코드를 설정 한 다음 사용자 지정 미들웨어 또는 UseStatusCodePagesWithReExecute를 통해 처리하는 것이 어떻습니까? 자세한 내용은 devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api 를 참조하십시오
Paul Hiles

4
콘텐츠 협상을 완전히 무시하고 항상 JSON으로 직렬화하기 때문에 좋지 않습니다.
Konrad

5
@Konrad 유효 지점. 그렇기 때문에이 예제는 최종 결과가 아니라 시작할 수있는 곳입니다. API의 99 %는 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 Middleware를 사용할 수 있으며 다음 은 몇 가지 예입니다. Elm Exception Middleware

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

그래도 문제가 해결되지 않으면 ExceptionHandlerMiddleware 및 ElmMiddleware의 구현을보고 자신 만의 미들웨어 구성 요소를 롤업하여 자신 만의 개념을 파악할 수 있습니다.

StatusCodePages 미들웨어 아래에 다른 모든 미들웨어 구성 요소 위에 예외 처리 미들웨어를 추가하는 것이 중요합니다. 이렇게하면 예외 미들웨어가 예외를 캡처하고 로그 한 다음 요청이 StatusCodePage 미들웨어로 진행하여 사용자에게 친숙한 오류 페이지가 표시되도록합니다.


천만에요. 또한 귀하의 요청을 더 잘 충족시킬 수있는 엣지 케이스에서 기본 UseStatusPages를 재정의하는 예제에 대한 링크를 제공했습니다.
Ashley Lee

1
Elm은 로그를 유지하지 않으므로 Serilog 또는 NLog를 사용하여 직렬화를 제공하는 것이 좋습니다. ELM 로그가 사라짐을
Michael Freidgeim

2
링크가 끊어졌습니다.
Mathias Lykkegaard Lorenzen

@AshleyLee, UseStatusCodePages웹 API 서비스 구현에 사용되는 질문입니다 . 조회수 또는 HTML이 전혀 없으며 JSON 응답 만 가능합니다.
Paul Michalik

23

잘 받아 들여진 대답은 많은 도움이되었지만 런타임에 오류 상태 코드를 관리하기 위해 미들웨어에서 HttpStatusCode를 전달하고 싶었습니다.

이 링크 에 따르면 똑같이 할 아이디어가 있습니다. 그래서 나는 Andrei Answer를 이것과 병합했습니다. 최종 코드는 다음과 같습니다.
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();

이제 계정 컨트롤러의 로그인 방법 :

 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;
        }

위의 사용자를 찾지 못한 경우 HttpStatusCode.NotFound 상태 및
미들웨어 의 사용자 정의 메시지를 전달한 HttpStatusCodeException을 발생시키는 지 확인할 수 있습니다.

catch (HttpStatusCodeException ex)

제어를 전달할 차단됨이 호출됩니다.

개인 작업 HandleExceptionAsync (HttpContext 컨텍스트, HttpStatusCodeException 예외) 메서드

.


하지만 전에 런타임 오류가 발생하면 어떻게해야합니까? 이를 위해 예외를 throw하고 catch (Exception exceptionObj) 블록에서 catch 블록을 사용하여 제어를 전달합니다.

Task HandleExceptionAsync (HttpContext 컨텍스트, 예외 예외)

방법.

균일 성을 위해 단일 ErrorDetails 클래스를 사용했습니다.


확장 방법을 어디에 두어야합니까? 불행하게도에서 startup.csvoid Configure(IapplicationBuilder app)오류가 발생합니다 IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. 그리고 어디에 참조를 추가했습니다 CustomExceptionMiddleware.cs.
Spedo De La Rossa 2018 년

당신은 당신의 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을 반환합니다).

내 접근 방식은 ObjectVC를 사용하여 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

먼저 Startup웹 서버의 오류 및 처리되지 않은 예외에 대해 오류 페이지로 다시 실행되도록 ASP.NET Core 2 를 구성 하십시오.

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));
})
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.