ASP.NET Web API 2의 전역 오류 처리

작성자 : David Matson, Rick Anderson

이 항목에서는 ASP.NET 4.x에 대한 ASP.NET Web API 2의 전역 오류 처리에 대한 개요를 제공합니다. 현재 Web API에서는 전역적으로 오류를 기록하거나 처리하는 쉬운 방법이 없습니다. 일부 처리되지 않은 예외는 예외 필터를 통해 처리할 수 있지만 예외 필터에서 처리할 수 없는 경우가 많습니다. 예를 들면 다음과 같습니다.

  1. 컨트롤러 생성자에서 throw된 예외
  2. 메시지 처리기에서 throw된 예외
  3. 라우팅 중에 throw된 예외
  4. 응답 콘텐츠를 직렬화하는 동안 throw된 예외

가능한 경우 이러한 예외를 기록하고 처리하는 간단하고 일관된 방법을 제공하려고 합니다.

예외를 처리하기 위한 두 가지 주요 사례가 있습니다. 오류 응답을 보낼 수 있는 경우와 예외를 기록하기만 하면 됩니다. 후자의 경우의 예는 스트리밍 응답 콘텐츠 중간에 예외가 throw되는 경우입니다. 이 경우 상태 코드, 헤더 및 부분 콘텐츠가 이미 유선으로 전달되었으므로 새 응답 메시지를 보내기에는 너무 늦으므로 연결을 중단하기만 하면 됩니다. 새 응답 메시지를 생성하기 위해 예외를 처리할 수는 없지만 예외 로깅은 계속 지원됩니다. 오류를 감지할 수 있는 경우 다음과 같이 적절한 오류 응답을 반환할 수 있습니다.

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

기존 옵션

예외 필터 외에도 메시지 처리기를 사용하여 500개 수준 응답을 모두 관찰할 수 있지만 원래 오류에 대한 컨텍스트가 부족하기 때문에 이러한 응답에 대한 작업은 어렵습니다. 또한 메시지 처리기에는 처리할 수 있는 사례와 관련된 예외 필터와 동일한 제한 사항이 있습니다. Web API에는 오류 조건을 캡처하는 추적 인프라가 있지만 추적 인프라는 진단 목적이며 프로덕션 환경에서 실행하기 위해 설계되거나 적합하지 않습니다. 전역 예외 처리 및 로깅은 프로덕션 중에 실행되고 기존 모니터링 솔루션(예: ELMAH)에 연결할 수 있는 서비스여야 합니다.

솔루션 개요

처리되지 않은 예외를 기록하고 처리하기 위해 두 개의 새로운 사용자 교체 가능 서비스인 IExceptionLogger 및 IExceptionHandler를 제공합니다. 서비스는 매우 유사하며 두 가지 기본 차이점이 있습니다.

  1. 여러 예외 로거를 등록하지만 단일 예외 처리기만 등록할 수 있습니다.
  2. 연결을 중단하려는 경우에도 예외 로거는 항상 호출됩니다. 예외 처리기는 보낼 응답 메시지를 선택할 수 있는 경우에만 호출됩니다.

두 서비스 모두 예외가 검색된 지점, 특히 HttpRequestMessage, HttpRequestContext, throw된 예외 및 예외 원본(아래 세부 정보)에서 관련 정보를 포함하는 예외 컨텍스트에 대한 액세스를 제공합니다.

디자인 원칙

  1. 호환성이 손상되는 변경 없음 이 기능은 부 릴리스에서 추가되기 때문에 솔루션에 영향을 주는 한 가지 중요한 제약 조건은 형식 계약 또는 동작에 대한 호환성이 손상되는 변경이 없다는 것입니다. 이 제약 조건은 예외를 500개의 응답으로 바꾸는 기존 catch 블록 측면에서 수행하려는 일부 정리를 배제했습니다. 이 추가 정리는 후속 주요 릴리스에 대해 고려할 수 있는 내용입니다.
  2. Web API 구문을 사용하여 일관성 유지 관리 Web API의 필터 파이프라인은 작업별, 컨트롤러별 또는 전역 scope 논리를 유연하게 적용할 수 있는 유연성으로 교차 절단 문제를 처리하는 좋은 방법입니다. 예외 필터를 포함한 필터에는 전역 scope 등록된 경우에도 항상 작업 및 컨트롤러 컨텍스트가 있습니다. 해당 계약은 필터에 적합하지만 전역적으로 범위가 지정된 필터조차도 작업 또는 컨트롤러 컨텍스트가 없는 메시지 처리기의 예외와 같은 일부 예외 처리 사례에 적합하지 않다는 것을 의미합니다. 예외 처리를 위해 필터에서 제공하는 유연한 범위 지정을 사용하려는 경우에도 예외 필터가 필요합니다. 그러나 컨트롤러 컨텍스트 외부에서 예외를 처리해야 하는 경우 전체 전역 오류 처리(컨트롤러 컨텍스트 및 작업 컨텍스트 제약 조건이 없는 항목)를 위한 별도의 구문도 필요합니다.

사용 시기

  • 예외 로거는 Web API에서 catch된 처리되지 않은 모든 예외를 확인하는 솔루션입니다.
  • 예외 처리기는 Web API에서 catch된 처리되지 않은 예외에 대해 가능한 모든 응답을 사용자 지정하기 위한 솔루션입니다.
  • 예외 필터는 특정 작업 또는 컨트롤러와 관련된 하위 집합 처리되지 않은 예외를 처리하는 가장 쉬운 솔루션입니다.

서비스 세부 정보

예외 로거 및 처리기 서비스 인터페이스는 각 컨텍스트를 사용하는 간단한 비동기 메서드입니다.

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

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

또한 이러한 인터페이스 모두에 대한 기본 클래스를 제공합니다. 코어(동기화 또는 비동기) 메서드를 재정의하는 것은 권장 시간에 기록하거나 처리하는 데 필요한 모든 것입니다. 로깅의 ExceptionLogger 경우 기본 클래스는 코어 로깅 메서드가 각 예외에 대해 한 번만 호출되도록 합니다(나중에 호출 스택을 추가로 전파하고 다시 catch되는 경우에도). 기본 클래스는 ExceptionHandler 호출 스택 맨 위에 있는 예외에 대해서만 핵심 처리 메서드를 호출하고 레거시 중첩된 catch 블록을 무시합니다. (이러한 기본 클래스의 간소화된 버전은 아래 부록에 있습니다.) 및 IExceptionHandler 는 둘 다 IExceptionLogger 를 통해 예외에 대한 정보를 받습니다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제공합니다. 단위 테스트를 제외하고 항상 을 RequestContext제공합니다. 예외 필터에 ControllerContext 대해 catch 블록에서 를 호출하는 경우에만 및 ActionContext 를 거의 제공하지 않습니다. 매우 드물게 을 Response제공합니다(응답을 작성하려고 할 때 특정 IIS의 경우에만). 이러한 속성 중 일부는 예외 클래스의 멤버에 null 액세스하기 전에 검사 소비자의 요일 수 null 있습니다.CatchBlock 는 예외가 발생한 catch 블록을 나타내는 문자열입니다. catch 블록 문자열은 다음과 같습니다.

  • HttpServer(SendAsync 메서드)

  • HttpControllerDispatcher(SendAsync 메서드)

  • HttpBatchHandler(SendAsync 메서드)

  • IExceptionFilter(ExecuteAsync의 예외 필터 파이프라인에 대한 ApiController의 처리)

  • OWIN 호스트:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync(버퍼링 출력용)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync(스트리밍 출력용)
  • 웹 호스트:

    • HttpControllerHandler.WriteBufferedResponseContentAsync(버퍼링 출력용)
    • HttpControllerHandler.WriteStreamedResponseContentAsync(스트리밍 출력용)
    • HttpControllerHandler.WriteErrorResponseContentAsync(버퍼링된 출력 모드에서 오류 복구 실패)

catch 블록 문자열 목록은 정적 읽기 전용 속성을 통해서도 사용할 수 있습니다. (핵심 catch 블록 문자열은 static ExceptionCatchBlocks에 있으며 나머지는 OWIN 및 웹 호스트에 대해 각각 하나의 정적 클래스에 표시됩니다.)IsTopLevelCatchBlock 는 호출 스택의 맨 위에만 예외를 처리하는 권장 패턴을 따르는 데 유용합니다. 중첩된 catch 블록이 발생하는 모든 위치에서 예외를 500개의 응답으로 전환하는 대신 예외 처리기는 예외가 호스트에서 표시될 때까지 전파되도록 할 수 있습니다.

ExceptionContext로거는 외에도 전체 ExceptionLoggerContext을 통해 정보를 하나 더 가져옵니다.

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

두 번째 속성인 CanBeHandled를 사용하면 로거가 처리할 수 없는 예외를 식별할 수 있습니다. 연결이 중단되고 새 응답 메시지를 보낼 수 없는 경우 로거가 호출되지만 처리기가 호출 되지 않으며 로거는 이 속성에서 이 시나리오를 식별할 수 있습니다.

에 더하여 처리기는 예외를 처리하기 위해 ExceptionContext전체 ExceptionHandlerContext 에서 설정할 수 있는 속성을 하나 더 가져옵니다.

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

예외 처리기는 속성을 작업 결과(예: ExceptionResult, InternalServerErrorResult, StatusCodeResult 또는 사용자 지정 결과)로 설정 Result 하여 예외를 처리했음을 나타냅니다. 속성이 Result null이면 예외가 처리되지 않고 원래 예외가 다시 throw됩니다.

호출 스택 맨 위에 있는 예외의 경우 API 호출자에게 응답이 적절한지 확인하기 위해 추가 단계를 수행했습니다. 예외가 호스트에 전파되는 경우 호출자는 노란색 사망 화면 또는 일반적으로 적절한 API 오류 응답이 아닌 일반적으로 HTML인 다른 호스트 제공 응답을 볼 수 있습니다. 이러한 경우 결과는 null이 아닌 것으로 시작되며, 사용자 지정 예외 처리기가 명시적으로 다시 null (처리되지 않음)로 설정한 경우에만 예외가 호스트에 전파됩니다. null 이러한 경우 를 로 설정 Result 하면 다음 두 가지 시나리오에 유용할 수 있습니다.

  1. 사용자 지정 예외 처리 미들웨어가 있는 OWIN 호스팅 Web API는 Web API 전후에 등록되었습니다.
  2. 브라우저를 통한 로컬 디버깅. 여기서 죽음의 노란색 화면은 실제로 처리되지 않은 예외에 대한 유용한 응답입니다.

예외 로거와 예외 처리기 모두에 대해 로거 또는 처리기 자체가 예외를 throw하는 경우 복구하기 위해 아무 것도 수행하지 않습니다. (예외가 전파되도록 하는 것 외에 더 나은 방법이 있는 경우 이 페이지의 맨 아래에 피드백을 남겨 둡니다.) 예외 로거 및 처리기에 대한 계약은 예외가 호출자에게 전파되도록 해서는 안 된다는 것입니다. 그렇지 않으면 예외가 전파되며, 종종 호스트에 전파되어 HTML 오류(예: ASP)가 발생합니다. NET의 노란색 화면)이 클라이언트로 다시 전송됩니다(일반적으로 JSON 또는 XML을 예상하는 API 호출자에게는 기본 설정 옵션이 아님).

예제

예외 로거 추적

아래 예외 로거는 구성된 추적 원본(Visual Studio의 디버그 출력 창 포함)에 예외 데이터를 보냅니다.

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

사용자 지정 오류 메시지 예외 처리기

아래 예외 처리기는 고객 지원에 문의하기 위한 이메일 주소를 포함하여 클라이언트에 대한 사용자 지정 오류 응답을 생성합니다.

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

예외 필터 등록

"ASP.NET MVC 4 웹 애플리케이션" 프로젝트 템플릿을 사용하여 프로젝트를 만드는 경우 Web API 구성 코드를 클래스 내에 WebApiConfigApp_Start 폴더에 배치합니다.

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

        // Other configuration code...
    }
}

부록: 기본 클래스 세부 정보

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