IT story

ASP.NET Core 웹 API 예외 처리

hot-time 2020. 4. 22. 08:11
반응형

ASP.NET Core 웹 API 예외 처리


수년간 일반 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와 같이 작동하는 것처럼 잡힐 것으로 기대했습니다.

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


예외 처리 미들웨어

다른 예외 처리 접근 방식으로 많은 실험을 한 후에 미들웨어를 사용했습니다. 내 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);
}));

이것은 예외 처리를 설정하는 매우 명백하지만 쉬운 방법은 아닙니다. 그러나 필요한 종속성을 주입하는 기능으로 더 많은 제어권을 가지기 때문에 미들웨어 접근 방식을 선호합니다.


가장 좋은 방법은 미들웨어를 사용하여 원하는 로깅을 얻는 것입니다. 예외 로깅을 하나의 미들웨어에 넣고 다른 미들웨어에서 사용자에게 표시되는 오류 페이지를 처리하려고합니다. 이를 통해 논리를 분리 할 수 ​​있고 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 미들웨어로 진행하여 사용자에게 친숙한 오류 페이지가 표시되도록합니다.


최신 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순서가 중요하므로 추가해야 합니다.


예외 유형별로 예외 처리 동작을 구성하려면 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();
}

잘 받아 들여진 대답은 많은 도움이되었지만 런타임에 오류 상태 코드를 관리하기 위해 미들웨어에서 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 클래스를 사용했습니다.


첫 번째로, 솔루션을 그의 모범에 근거한 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;
        }
    }
}

먼저 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....

참고 URL : https://stackoverflow.com/questions/38630076/asp-net-core-web-api-exception-handling

반응형