ASP.NET 웹 API 2의 글로벌 오류 처리Global Error Handling in ASP.NET Web API 2

데이비드 매슨, 릭 앤더슨by David Matson, Rick Anderson

이 항목에서는 ASP.NET 4.x에 대한 ASP.NET 웹 API 2의 전역 오류 처리에 대한 개요를 제공합니다.This topic provides an overview of global error handling in ASP.NET Web API 2 for ASP.NET 4.x. 오늘날 웹 API에서는 전 세계적으로 오류를 기록하거나 처리할 수 있는 쉬운 방법이 없습니다.Today there's no easy way in Web API to log or handle errors globally. 처리되지 않은 일부 예외는 예외 필터를통해 처리할 수 있지만 예외 필터가 처리할 수 없는 경우가 많습니다.Some unhandled exceptions can be processed via exception filters, but there are a number of cases that exception filters can't handle. 예를 들어:For example:

  1. 컨트롤러 생성자에서 throw된 예외Exceptions thrown from controller constructors.
  2. 메시지 처리기에서 throw된 예외Exceptions thrown from message handlers.
  3. 라우팅 중에 throw된 예외Exceptions thrown during routing.
  4. 응답 콘텐츠를 직렬화하는 동안 throw된 예외Exceptions thrown during response content serialization.

이러한 예외를 기록하고 처리하는 간단하고 일관된 방법을 제공하고자 합니다.We want to provide a simple, consistent way to log and handle (where possible) these exceptions.

예외를 처리하는 두 가지 주요 사례가 있습니다.There are two major cases for handling exceptions, the case where we are able to send an error response and the case where all we can do is log the exception. 후자의 경우는 스트리밍 응답 콘텐츠 중간에 예외가 throw되는 경우입니다. 이 경우 상태 코드, 헤더 및 부분 콘텐츠가 이미 와이어를 가로 질러 이동했기 때문에 새 응답 메시지를 보내기에는 너무 늦었기 때문에 연결을 중단하기만 하면 됩니다.An example for the latter case is when an exception is thrown in the middle of streaming response content; in that case it is too late to send a new response message since the status code, headers, and partial content have already gone across the wire, so we simply abort the connection. 새 응답 메시지를 생성하기 위해 예외를 처리할 수 없지만 예외 로깅은 계속 지원됩니다.Even though the exception can't be handled to produce a new response message, we still support logging the exception. 오류를 감지할 수 있는 경우 다음과 같이 적절한 오류 응답을 반환할 수 있습니다.In cases where we can detect an error, we can return an appropriate error response as shown in the following:

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

기존 옵션Existing Options

예외 필터외에도 메시지 처리기를 사용하여 모든 500수준 응답을 관찰할 수 있지만 원래 오류에 대한 컨텍스트가 없기 때문에 이러한 응답에 대해 사용하기가 어렵습니다.In addition to exception filters, message handlers can be used today to observe all 500-level responses, but acting on those responses is difficult, as they lack context about the original error. 메시지 처리기는 처리할 수 있는 서비스 케이스에 대한 예외 필터와 동일한 몇 가지 제한 사항이 있습니다.Message handlers also have some of the same limitations as exception filters regarding the cases they can handle. Web API에는 오류 조건을 캡처하는 추적 인프라가 있지만 추적 인프라는 진단을 위한 것이며 프로덕션 환경에서 실행하기 위해 설계되거나 적합하지 않습니다.While Web API does have tracing infrastructure that captures error conditions the tracing infrastructure is for diagnostics purposes and is not designed or suited for running in production environments. 전역 예외 처리 및 로깅은 프로덕션 중에 실행되고 기존 모니터링 솔루션(예: ELMAH)에연결될 수 있는 서비스여야 합니다.Global exception handling and logging should be services that can run during production and be plugged into existing monitoring solutions (for example, ELMAH).

솔루션 개요Solution Overview

처리되지 않은 예외를 기록하고 처리하기 위해 두 개의 새로운 사용자 교체 가능한 서비스인 IExceptionLogger 및 IExceptionHandlerHandler를 제공합니다.We provide two new user-replaceable services, IExceptionLogger and IExceptionHandler, to log and handle unhandled exceptions. 서비스는 매우 유사하며 두 가지 주요 차이점이 있습니다.The services are very similar, with two main differences:

  1. 여러 예외 로거를 등록하지만 단일 예외 처리기만 등록할 수 있습니다.We support registering multiple exception loggers but only a single exception handler.
  2. 연결을 중단하려고 하는 경우에도 예외 로거가 항상 호출됩니다.Exception loggers always get called, even if we're about to abort the connection. 예외 처리기는 보낼 응답 메시지를 선택할 수 있는 경우에만 호출됩니다.Exception handlers only get called when we're still able to choose which response message to send.

두 서비스 모두 예외가 검색된 시점부터 관련 정보를 포함하는 예외 컨텍스트, 특히 HttpRequestMessage, HttpRequestContext,throw된 예외 및 예외 소스(아래 세부 정보)에 대한 액세스를 제공합니다.Both services provide access to an exception context containing relevant information from the point where the exception was detected, particularly the HttpRequestMessage, the HttpRequestContext, the thrown exception and the exception source (details below).

디자인 원칙Design Principles

  1. 주요 변경 사항 없음 이 기능은 부 릴리스에 추가되기 때문에 솔루션에 영향을 주는 한 가지 중요한 제약 조건은 계약을 입력하거나 동작에 대한 주요 변경 내용이 없다는 것입니다.No breaking changes Because this functionality is being added in a minor release, one important constraint impacting the solution is that there be no breaking changes, either to type contracts or to behavior. 이 제약 조건은 예외를 500개의 응답으로 전환하는 기존 catch 블록 측면에서 수행하려는 일부 정리를 배제했습니다.This constraint ruled out some cleanup we would like to have done in terms of existing catch blocks turning exceptions into 500 responses. 이 추가 정리는 후속 주요 릴리스에서 고려할 수 있는 사항입니다.This additional cleanup is something we might consider for a subsequent major release. 이것이 중요한 경우 웹 API 사용자 음성으로투표하십시오 ASP.NET.If this is important to you please vote on it at ASP.NET Web API user voice.
  2. 웹 API 구문과의 일관성 유지 Web API의 필터 파이프라인은 작업별, 컨트롤러별 또는 전역 범위에서 논리를 유연하게 적용하여 교차 절단 문제를 처리할 수 있는 좋은 방법입니다.Maintaining consistency with Web API constructs Web API's filter pipeline is a great way to handle cross-cutting concerns with the flexibility of applying the logic at an action-specific, controller-specific or global scope. 예외 필터를 포함한 필터에는 전역 범위에 등록된 경우에도 항상 작업 및 컨트롤러 컨텍스트가 있습니다.Filters, including exception filters, always have action and controller contexts, even when registered at the global scope. 이 계약은 필터에 적합하지만 전역으로 범위가 조정된 예외 필터도 작업 또는 컨트롤러 컨텍스트가 없는 메시지 처리기의 예외와 같은 일부 예외 처리 사례에 적합하지 않다는 것을 의미합니다.That contract makes sense for filters, but it means that exception filters, even globally scoped ones, aren't a good fit for some exception handling cases, such as exceptions from message handlers, where no action or controller context exists. 예외 처리를 위해 필터에서 제공하는 유연한 범위 지정을 사용하려면 예외 필터가 필요합니다.If we want to use the flexible scoping afforded by filters for exception handling, we still need exception filters. 그러나 컨트롤러 컨텍스트 외부에서 예외를 처리해야 하는 경우 전체 전역 오류 처리(컨트롤러 컨텍스트 및 작업 컨텍스트 제약 조건이 없는 항목)에 대해 별도의 구문이 필요합니다.But if we need to handle exception outside of a controller context, we also need a separate construct for full global error handling (something without the controller context and action context constraints).

사용 시기When to Use

  • 예외 로거는 Web API에서 catch된 처리되지 않은 모든 예외를 확인하는 솔루션입니다.Exception loggers are the solution to seeing all unhandled exception caught by Web API.
  • 예외 처리기는 Web API에서 catch한 처리되지 않은 예외에 대해 가능한 모든 응답을 사용자 지정하는 솔루션입니다.Exception handlers are the solution for customizing all possible responses to unhandled exceptions caught by Web API.
  • 예외 필터는 특정 작업 또는 컨트롤러와 관련된 하위 집합처리되지 않은 예외를 처리하는 가장 쉬운 솔루션입니다.Exception filters are the easiest solution for processing the subset unhandled exceptions related to a specific action or controller.

서비스 세부 정보Service Details

예외 로거 및 처리기 서비스 인터페이스는 각각의 컨텍스트를 취하는 간단한 비동기 메서드입니다.The exception logger and handler service interfaces are simple async methods taking the respective contexts:

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

또한 이러한 두 인터페이스모두에 대한 기본 클래스도 제공합니다.We also provide base classes for both of these interfaces. 코어(동기화 또는 비동기) 메서드를 재정의하는 것은 권장 시간에 로그하거나 처리하는 데 필요한 모든 것입니다.Overriding the core (sync or async) methods is all that is required to log or handle at the recommended times. 로깅의 ExceptionLogger 경우 기본 클래스는 코어 로깅 메서드가 각 예외에 대해 한 번만 호출되도록 합니다(나중에 호출 스택을 더 많이 전파하고 다시 catch되는 경우에도 마찬가지입니다).For logging, the ExceptionLogger base class will ensure that the core logging method is only called once for each exception (even if it later propagates further up the call stack and is caught again). 기본 ExceptionHandler 클래스는 레거시 중첩된 catch 블록을 무시하고 호출 스택 맨 위에 있는 예외에 대해서만 코어 처리 메서드를 호출합니다.The ExceptionHandler base class will call the core handling method only for exceptions at the top of the call stack, ignoring legacy nested catch blocks. (이러한 기본 클래스의 단순화된 버전은 아래 부록에 있습니다.) 둘 IExceptionLogger IExceptionHandler 다 를 통해 예외에 대한 정보를 수신합니다. ExceptionContext(Simplified versions of these base classes are in the appendix below.) Both IExceptionLogger and IExceptionHandler receive information about the exception via an ExceptionContext.

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

프레임워크가 예외 로거 또는 예외 처리기를 호출할 ExceptionRequest항상 및 을 제공합니다.When the framework calls an exception logger or an exception handler, it will always provide an Exception and a Request. 단위 테스트를 제외하고 항상 RequestContext을 제공합니다.Except for unit testing, it will also always provide a RequestContext. 예외 필터에 ControllerContext 대한 ActionContext catch 블록에서 호출하는 경우에만 a 및 를 거의 제공하지 않습니다.It will rarely provide a ControllerContext and ActionContext (only when calling from the catch block for exception filters). 그것은 매우 드물게 Response(응답을 작성하려고하는 중간에 특정 IIS의 경우에만)를 제공하지 않습니다.It will very rarely provide a Response(only in certain IIS cases when in the middle of trying to write the response). 이러한 속성 중 일부는 예외 null 클래스의 멤버에 null 액세스하기 전에 확인하는 것은 소비자의 책임이 될 수 있으므로 주의하십시오.CatchBlockNote that because some of these properties may be null it is up to the consumer to check for null before accessing members of the exception class.CatchBlock 는 예외를 본 catch 블록을 나타내는 문자열입니다.is a string indicating which catch block saw the exception. catch 블록 문자열은 다음과 같습니다.The catch block strings are as follows:

  • HttpServer (센드애싱크 방법)HttpServer (SendAsync method)

  • HttpControllerDispatcher(SendAsync 메서드)HttpControllerDispatcher (SendAsync method)

  • HttpBatchHandler (센타싱크 방법)HttpBatchHandler (SendAsync method)

  • IExceptionFilter(ApiController의 ExecuteAsync에서 예외 필터 파이프라인 처리)IExceptionFilter (ApiController's processing of the exception filter pipeline in ExecuteAsync)

  • 오윈 호스트:OWIN host:

    • HttpMessageHandlerAdapter.버퍼응답콘텐츠Async(버퍼링 출력용)HttpMessageHandlerAdapter.BufferResponseContentAsync (for buffering output)
    • HttpMessageHandlerAdapter.CopyResponse콘텐츠Async(스트리밍 출력용)HttpMessageHandlerAdapter.CopyResponseContentAsync (for streaming output)
  • 웹 호스트:Web host:

    • HttpControllerHandler.WriteBufferedResponse콘텐츠Async(버퍼링 출력용)HttpControllerHandler.WriteBufferedResponseContentAsync (for buffering output)
    • HttpControllerHandler.WriteStreamedResponse콘텐츠동기화(스트리밍 출력용)HttpControllerHandler.WriteStreamedResponseContentAsync (for streaming output)
    • HttpControllerHandler.WriteErrorResponseContentAsync (버퍼링된 출력 모드에서 오류 복구 실패의 경우)HttpControllerHandler.WriteErrorResponseContentAsync (for failures in error recovery under buffered output mode)

catch 블록 문자열 목록은 정적 readonly 속성을 통해서도 사용할 수 있습니다.The list of catch block strings is also available via static readonly properties. (코어 캐치 블록 문자열은 정적 ExceptionCatchBlocks에 있고 나머지는 OWIN 및 웹 호스트에 대해 각각 하나의 정적 클래스에 나타납니다.)IsTopLevelCatchBlock(The core catch block string are on the static ExceptionCatchBlocks; the remainder appear on one static class each for OWIN and web host).IsTopLevelCatchBlock 호출 스택맨 맨 위에있는 예외를 처리하는 권장 패턴을 따르는 데 유용합니다.is helpful for following the recommended pattern of handling exceptions only at the top of the call stack. 예외를 중첩된 catch 블록이 발생하는 모든 곳에서 500개의 응답으로 전환하는 대신 예외 처리기는 호스트에서 보일 때까지 예외를 전파할 수 있습니다.Rather than turning exceptions into 500 responses anywhere a nested catch block occurs, an exception handler can let exceptions propagate until they are about to be seen by the host.

뿐만 ExceptionContext아니라, 로거는 전체를 ExceptionLoggerContext통해 하나의 정보를 가져옵니다 :In addition to the ExceptionContext, a logger gets one more piece of information via the full ExceptionLoggerContext:

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

두 번째 CanBeHandled속성 , "로거는 처리할 수 없는 예외를 식별할 수 있습니다.The second property, CanBeHandled, allows a logger to identify an exception that cannot be handled. 연결이 중단되고 새 응답 메시지를 보낼 수 없는 경우 로거가 호출되지만 처리기가 호출되지 않으며 로거가 이 속성에서 이 시나리오를 식별할 수 있습니다.When the connection is about to be aborted and no new response message can be sent, the loggers will be called but the handler will not be called, and the loggers can identify this scenario from this property.

ExceptionContext에 추가로 처리기는 예외를 처리하기 위해 전체에 ExceptionHandlerContext 설정할 수 있는 속성을 하나 더 가져옵니다.In additional to the ExceptionContext, a handler gets one more property it can set on the full ExceptionHandlerContext to handle the exception:

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

Result 예외 처리기는 속성을 작업 결과(예: ExceptionResult, InternalServerErrorResult, StatusCodeResult또는 사용자 지정 결과)로 설정하여 예외를 처리했음을 나타냅니다.An exception handler indicates that it has handled an exception by setting the Result property to an action result (for example, an ExceptionResult, InternalServerErrorResult, StatusCodeResult, or a custom result). 속성이 Result null이면 예외가 처리되지 않고 원래 예외가 다시 throw됩니다.If the Result property is null, the exception is unhandled and the original exception will be re-thrown.

호출 스택 맨 위에 있는 예외의 경우 API 호출자에게 응답이 적절한지 확인하기 위해 추가 단계를 밟습니다.For exceptions at the top of the call stack, we took an extra step to ensure the response is appropriate for API callers. 예외가 호스트에 전파되면 호출자는 노란색 사망 화면또는 일반적으로 적절한 API 오류 응답이 아닌 HTML인 다른 호스트가 제공된 응답을 볼 수 있습니다.If the exception propagates up to the host, the caller would see the yellow screen of death or some other host provided response which is typically HTML and not usually an appropriate API error response. 이러한 경우 Result는 null이 아닌 것으로 시작되며 사용자 지정 예외 처리기가 명시적으로 다시 (처리되지 않음)으로 null 설정하는 경우에만 예외가 호스트에 전파됩니다.In these cases, the Result starts out non-null, and only if a custom exception handler explicitly sets it back to null (unhandled) will the exception propagate to the host. 이러한 Result null 경우설정은 다음 두 가지 시나리오에 유용할 수 있습니다.Setting Result to null in such cases can be useful for two scenarios:

  1. OWIN은 웹 API 이전/외부에 등록된 미들웨어를 처리하는 사용자 지정 예외를 가진 웹 API를 호스팅했습니다.OWIN hosted Web API with custom exception handling middleware registered before/outside Web API.
  2. 노란색 사망 화면이 실제로 처리되지 않은 예외에 대한 유용한 응답인 브라우저를 통한 로컬 디버깅입니다.Local debugging via a browser, where the yellow screen of death is actually a helpful response for an unhandled exception.

예외 로거와 예외 처리기 모두에 대해 로거 또는 처리기 자체가 예외를 throw하는 경우 복구할 수 없습니다.For both exception loggers and exception handlers, we don't do anything to recover if the logger or handler itself throws an exception. (예외가 전파되도록 하는 것 이외에는 더 나은 접근 방식이 있는 경우 이 페이지의 맨 아래에 피드백을 남겨 주세요.) 예외 로거 및 처리기에 대한 계약은 예외가 호출자에게 전파되도록 해서는 안 된다는 것입니다. 그렇지 않으면 예외가 호스트로 전파되어 ASP와 같은 HTML 오류가 발생하는 경우가 많습니다. NET의 노란색 화면)이 클라이언트로 다시 전송됩니다(일반적으로 JSON 또는 XML을 기대하는 API 호출자에게는 선호되지 않습니다).(Other than letting the exception propagate, leave feedback at the bottom of this page if you have a better approach.) The contract for exception loggers and handlers is that they should not let exceptions propagate up to their callers; otherwise, the exception will just propagate, often all the way to the host resulting in an HTML error (like ASP.NET's yellow screen) being sent back to the client (which usually isn't the preferred option for API callers that expect JSON or XML).

Examples

추적 예외 로거Tracing Exception Logger

아래 예외 로거는 구성된 추적 원본(Visual Studio의 디버그 출력 창 포함)에 예외 데이터를 보냅니다.The exception logger below sends exception data to configured Trace sources (including the Debug output window in Visual Studio).

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

사용자 지정 오류 메시지 예외 처리기Custom Error Message Exception Handler

아래 예외 처리기는 지원에 문의하기 위한 전자 메일 주소를 포함하여 클라이언트에 대한 사용자 지정 오류 응답을 생성합니다.The exception handler below produces a custom error response to clients, including an email address for contacting support.

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

예외 필터 등록Registering Exception Filters

"ASP.NET MVC 4 웹 응용 프로그램" 프로젝트 템플릿을 사용하여 프로젝트를 만드는 경우 WebApiConfig App_Start 폴더에 웹 API 구성 코드를 클래스 안에 넣습니다.If you use the "ASP.NET MVC 4 Web Application" project template to create your project, put your Web API configuration code inside the WebApiConfig class, in the App_Start folder:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

부록: 기본 클래스 세부 정보Appendix: Base Class Details

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}