[C#] HttpResponseException을 던지거나 Request.CreateErrorResponse를 반환 하시겠습니까?

ASP.NET 웹 API의 예외 처리 기사를 검토 한 후 예외 를 던질 때와 오류 응답을 반환하는 시점에 대해 약간 혼란스러워합니다. 또한 메소드가 대신 도메인 특정 모델을 반환 할 때 응답을 수정할 수 있는지 궁금합니다 HttpResponseMessage.

그래서, 여기에 요약하자면 내 질문과 사례 번호가있는 코드가 있습니다.

질문

사례 # 1 관련 질문

  1. HttpResponseMessage메시지를 사용자 정의 할 수 있도록 항상 구체적 도메인 모델 대신 사용해야합니까 ?
  2. 구체적 도메인 모델을 반환하는 경우 메시지를 사용자 정의 할 수 있습니까?

사례 # 2,3,4에 관한 질문

  1. 예외를 던지거나 오류 응답을 반환해야합니까? 대답이 “그것에 달려있다”면, 당신은 상황과 예를 하나를 사용할 때와 다른 것을 사용할 수있는 시점에 줄 수 있습니다.
  2. 던지는 사이의 차이 무엇입니까 HttpResponseException대는 Request.CreateErrorResponse? 클라이언트에 대한 출력은 동일하게 보입니다 …
  3. HttpError응답 메시지를 항상 오류 ( “예외가 발생하는지 또는 오류 응답이 반환되는지)로”랩 ” 하는 데 사용해야합니까 ?

사례 샘플

// CASE #1
public Customer Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
        throw new HttpResponseException(notFoundResponse);
    }
    //var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    //response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return customer;
}

// CASE #2
public HttpResponseMessage Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
        throw new HttpResponseException(notFoundResponse);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return response;
}

// CASE #3
public HttpResponseMessage Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var message = String.Format("customer with id: {0} was not found", id);
        var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
        throw new HttpResponseException(errorResponse);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return response;
}

// CASE #4
public HttpResponseMessage Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var message = String.Format("customer with id: {0} was not found", id);
        var httpError = new HttpError(message);
        return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return response;
}

최신 정보

사례 # 2,3,4를 추가로 보여주기 위해 다음 코드 스 니펫은 고객을 찾을 수 없을 때 발생할 수있는 몇 가지 옵션을 강조합니다.

if (customer == null)
{
    // which of these 4 options is the best strategy for Web API?

    // option 1 (throw)
    var notFoundMessage = new HttpResponseMessage(HttpStatusCode.NotFound);
    throw new HttpResponseException(notFoundMessage);

    // option 2 (throw w/ HttpError)
    var message = String.Format("Customer with id: {0} was not found", id);
    var httpError = new HttpError(message);
    var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
    throw new HttpResponseException(errorResponse);

    // option 3 (return)
    var message = String.Format("Customer with id: {0} was not found", id);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
    // option 4 (return w/ HttpError)
    var message = String.Format("Customer with id: {0} was not found", id);
    var httpError = new HttpError(message);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
}



답변

내가 취한 접근법은 API 컨트롤러 작업에서 예외를 throw하고 예외를 처리하고 작업 실행 컨텍스트에 적절한 응답을 설정하는 예외 필터를 등록하는 것입니다.

필터는 전역 구성으로 필터를 등록하기 전에 특정 유형의 예외에 대한 핸들러를 등록하는 수단을 제공하는 유연한 인터페이스를 제공합니다.

이 필터를 사용하면 컨트롤러 조치에 분산시키는 대신 중앙 집중식 예외 처리가 가능합니다. 그러나 컨트롤러 작업 내에서 예외를 포착하고 특정 예외 처리를 중앙 집중화하는 것이 적합하지 않은 경우 특정 응답을 반환하는 경우가 있습니다.

필터 등록 예 :

GlobalConfiguration.Configuration.Filters.Add(
    new UnhandledExceptionFilterAttribute()
    .Register<KeyNotFoundException>(HttpStatusCode.NotFound)

    .Register<SecurityException>(HttpStatusCode.Forbidden)

    .Register<SqlException>(
        (exception, request) =>
        {
            var sqlException = exception as SqlException;

            if (sqlException.Number > 50000)
            {
                var response            = request.CreateResponse(HttpStatusCode.BadRequest);
                response.ReasonPhrase   = sqlException.Message.Replace(Environment.NewLine, String.Empty);

                return response;
            }
            else
            {
                return request.CreateResponse(HttpStatusCode.InternalServerError);
            }
        }
    )
);

UnhandledExceptionFilterAttribute 클래스 :

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http.Filters;

namespace Sample
{
    /// <summary>
    /// Represents the an attribute that provides a filter for unhandled exceptions.
    /// </summary>
    public class UnhandledExceptionFilterAttribute : ExceptionFilterAttribute
    {
        #region UnhandledExceptionFilterAttribute()
        /// <summary>
        /// Initializes a new instance of the <see cref="UnhandledExceptionFilterAttribute"/> class.
        /// </summary>
        public UnhandledExceptionFilterAttribute() : base()
        {

        }
        #endregion

        #region DefaultHandler
        /// <summary>
        /// Gets a delegate method that returns an <see cref="HttpResponseMessage"/> 
        /// that describes the supplied exception.
        /// </summary>
        /// <value>
        /// A <see cref="Func{Exception, HttpRequestMessage, HttpResponseMessage}"/> delegate method that returns 
        /// an <see cref="HttpResponseMessage"/> that describes the supplied exception.
        /// </value>
        private static Func<Exception, HttpRequestMessage, HttpResponseMessage> DefaultHandler = (exception, request) =>
        {
            if(exception == null)
            {
                return null;
            }

            var response            = request.CreateResponse<string>(
                HttpStatusCode.InternalServerError, GetContentOf(exception)
            );
            response.ReasonPhrase   = exception.Message.Replace(Environment.NewLine, String.Empty);

            return response;
        };
        #endregion

        #region GetContentOf
        /// <summary>
        /// Gets a delegate method that extracts information from the specified exception.
        /// </summary>
        /// <value>
        /// A <see cref="Func{Exception, String}"/> delegate method that extracts information 
        /// from the specified exception.
        /// </value>
        private static Func<Exception, string> GetContentOf = (exception) =>
        {
            if (exception == null)
            {
                return String.Empty;
            }

            var result  = new StringBuilder();

            result.AppendLine(exception.Message);
            result.AppendLine();

            Exception innerException = exception.InnerException;
            while (innerException != null)
            {
                result.AppendLine(innerException.Message);
                result.AppendLine();
                innerException = innerException.InnerException;
            }

            #if DEBUG
            result.AppendLine(exception.StackTrace);
            #endif

            return result.ToString();
        };
        #endregion

        #region Handlers
        /// <summary>
        /// Gets the exception handlers registered with this filter.
        /// </summary>
        /// <value>
        /// A <see cref="ConcurrentDictionary{Type, Tuple}"/> collection that contains 
        /// the exception handlers registered with this filter.
        /// </value>
        protected ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> Handlers
        {
            get
            {
                return _filterHandlers;
            }
        }
        private readonly ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> _filterHandlers = new ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>>();
        #endregion

        #region OnException(HttpActionExecutedContext actionExecutedContext)
        /// <summary>
        /// Raises the exception event.
        /// </summary>
        /// <param name="actionExecutedContext">The context for the action.</param>
        public override void OnException(HttpActionExecutedContext actionExecutedContext)
        {
            if(actionExecutedContext == null || actionExecutedContext.Exception == null)
            {
                return;
            }

            var type    = actionExecutedContext.Exception.GetType();

            Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null;

            if (this.Handlers.TryGetValue(type, out registration))
            {
                var statusCode  = registration.Item1;
                var handler     = registration.Item2;

                var response    = handler(
                    actionExecutedContext.Exception.GetBaseException(),
                    actionExecutedContext.Request
                );

                // Use registered status code if available
                if (statusCode.HasValue)
                {
                    response.StatusCode = statusCode.Value;
                }

                actionExecutedContext.Response  = response;
            }
            else
            {
                // If no exception handler registered for the exception type, fallback to default handler
                actionExecutedContext.Response  = DefaultHandler(
                    actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request
                );
            }
        }
        #endregion

        #region Register<TException>(HttpStatusCode statusCode)
        /// <summary>
        /// Registers an exception handler that returns the specified status code for exceptions of type <typeparamref name="TException"/>.
        /// </summary>
        /// <typeparam name="TException">The type of exception to register a handler for.</typeparam>
        /// <param name="statusCode">The HTTP status code to return for exceptions of type <typeparamref name="TException"/>.</param>
        /// <returns>
        /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler has been added.
        /// </returns>
        public UnhandledExceptionFilterAttribute Register<TException>(HttpStatusCode statusCode)
            where TException : Exception
        {

            var type    = typeof(TException);
            var item    = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
                statusCode, DefaultHandler
            );

            if (!this.Handlers.TryAdd(type, item))
            {
                Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;

                if (this.Handlers.TryRemove(type, out oldItem))
                {
                    this.Handlers.TryAdd(type, item);
                }
            }

            return this;
        }
        #endregion

        #region Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler)
        /// <summary>
        /// Registers the specified exception <paramref name="handler"/> for exceptions of type <typeparamref name="TException"/>.
        /// </summary>
        /// <typeparam name="TException">The type of exception to register the <paramref name="handler"/> for.</typeparam>
        /// <param name="handler">The exception handler responsible for exceptions of type <typeparamref name="TException"/>.</param>
        /// <returns>
        /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception <paramref name="handler"/> 
        /// has been added.
        /// </returns>
        /// <exception cref="ArgumentNullException">The <paramref name="handler"/> is <see langword="null"/>.</exception>
        public UnhandledExceptionFilterAttribute Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler)
            where TException : Exception
        {
            if(handler == null)
            {
              throw new ArgumentNullException("handler");
            }

            var type    = typeof(TException);
            var item    = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
                null, handler
            );

            if (!this.Handlers.TryAdd(type, item))
            {
                Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;

                if (this.Handlers.TryRemove(type, out oldItem))
                {
                    this.Handlers.TryAdd(type, item);
                }
            }

            return this;
        }
        #endregion

        #region Unregister<TException>()
        /// <summary>
        /// Unregisters the exception handler for exceptions of type <typeparamref name="TException"/>.
        /// </summary>
        /// <typeparam name="TException">The type of exception to unregister handlers for.</typeparam>
        /// <returns>
        /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler 
        /// for exceptions of type <typeparamref name="TException"/> has been removed.
        /// </returns>
        public UnhandledExceptionFilterAttribute Unregister<TException>()
            where TException : Exception
        {
            Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> item = null;

            this.Handlers.TryRemove(typeof(TException), out item);

            return this;
        }
        #endregion
    }
}

소스 코드는 여기 에서도 찾을 수 있습니다 .


답변

HttpResponseMessage를 반환하지 않고 엔티티 / 모델 클래스를 직접 반환하는 경우 유용한 방법은 컨트롤러에 다음 유틸리티 함수를 추가하는 것입니다.

private void ThrowResponseException(HttpStatusCode statusCode, string message)
{
    var errorResponse = Request.CreateErrorResponse(statusCode, message);
    throw new HttpResponseException(errorResponse);
}

적절한 상태 코드와 메시지로 간단히 호출하십시오.


답변

사례 # 1

  1. 반드시 파이프 라인에 응답 (작업 필터, 메시지 처리기)을 수정하는 다른 위치가있는 것은 아닙니다.
  2. 위를 참조하십시오.하지만 액션이 도메인 모델을 반환 하면 액션 내부 의 응답을 수정할 수 없습니다 .

사례 # 2-4

  1. HttpResponseException을 발생시키는 주요 이유는 다음과 같습니다.
    • 도메인 모델을 반환하지만 오류 사례를 처리해야하는 경우,
    • 오류를 예외로 처리하여 컨트롤러 논리를 단순화
  2. 이것들은 동등해야합니다. HttpResponseException은 HTTP 응답으로 반환되는 HttpResponseMessage를 캡슐화합니다.

    예를 들어, 사례 # 2는 다음과 같이 다시 작성할 수 있습니다.

    public HttpResponseMessage Get(string id)
    {
        HttpResponseMessage response;
        var customer = _customerService.GetById(id);
        if (customer == null)
        {
            response = new HttpResponseMessage(HttpStatusCode.NotFound);
        }
        else
        {
            response = Request.CreateResponse(HttpStatusCode.OK, customer);
            response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
        }
        return response;
    }

    …하지만 컨트롤러 로직이 더 복잡한 경우 예외를 발생 시키면 코드 흐름이 단순화 될 수 있습니다.

  3. HttpError는 응답 본문에 일관된 형식을 제공하며 JSON / XML / etc로 직렬화 할 수 있지만 필수는 아닙니다. 예를 들어 응답에 엔터티 본문을 포함하지 않거나 다른 형식을 원할 수 있습니다.


답변

HttpResponseException을 던지거나 오류에 대한 HttpResponesMessage를 반환하지 않음 – 를 제외하고 의도하는 경우 요청 종료정확히 일치하는 결과를 .

HttpResponseException은 다른 예외와 동일하게 처리되지 않습니다 . 그들은되는 예외 필터에서 잡은 없습니다 . 이들은 예외 처리기에서 발견되지 않습니다 . 그것들은 현재 코드의 실행 흐름을 종료하면서 HttpResponseMessage를 미끄러 뜨리는 교활한 방법입니다.

코드가이 특수 처리에 의존하는 인프라 코드가 아닌 경우 HttpResponseException 유형을 사용 하지 마십시오 !

HttpResponseMessage는 예외가 아닙니다. 현재 코드의 실행 흐름을 종료하지 않습니다. 예외로 필터링 할 수 없습니다 . 예외로 기록 할 수 없습니다 . 그것들은 유효한 결과를 나타냅니다. 500 개의 응답조차도 “올바른 비 예외 응답”입니다!


인생을 더 단순하게 만드십시오 :

예외 / 오류 사례가있는 경우 일반 .NET 예외 또는 사용자 정의 된 애플리케이션 예외 유형 ( 하지 이러한 상태 코드로 ‘HTTP 오류 / 응답의 속성 원하는과 HttpResponseException에서 파생를) -에 따라 일반 예외 취급 .

예외 필터 / 예외 처리기 / 예외 로거를 사용하여 다음과 같은 예외적 인 상황에 적합한 작업을 수행하십시오. 상태 코드 변경 / 추가? 추적 식별자를 추가 하시겠습니까? 스택 트레이스를 포함 하시겠습니까? 로그?

HttpResponseException을 피함으로써 ‘예외 사례’처리가 균일 해지고 노출 된 파이프 라인의 일부로 처리 될 수 있습니다! 예를 들어 응용 프로그램 수준 예외를 사용하여 ‘NotFound’를 404로, ‘ArgumentException’을 400으로, ‘NullReference’를 500으로 쉽고 균일하게 바꿀 수 있습니다. 반면에 확장 성은 오류 로깅과 같은 “기본”을 제공 할 수 있습니다.


답변

HttpResponseException대신에 사용 하는 경우에 대한 또 다른 경우Response.CreateResponse(HttpStatusCode.NotFound)조치 필터에 트랜잭션이 있고 클라이언트에 오류 응답을 리턴 할 때 트랜잭션을 롤백하려는 경우, 또는 기타 오류 상태 코드 가 있습니다.

를 사용 Response.CreateResponse하면 트랜잭션이 롤백되지 않고 예외가 발생합니다.


답변

webapi 2 메서드에서 HttpResponseMessage를 반환하는 대신 HttpResponseException을 throw하면 IIS Express에 즉시 호출하면 시간 초과 또는 200이 반환되지만 HTML 오류가 발생합니다. 응답. 이것을 테스트하는 가장 쉬운 방법은 HttpResponseException을 발생시키는 메소드를 $ .ajax 호출하고 아약스의 errorCallBack에서 다른 메소드 또는 간단한 http 페이지를 즉시 호출하는 것입니다. 즉시 통화가 실패 함을 알 수 있습니다. 오류 호출에 중단 점 또는 settimeout ()을 추가하여 두 번째 호출을 1 ~ 2 초 지연시켜 서버가 복구 할 시간을주는 것이 올바르게 작동합니다.

최신 정보:잘못된 Ajax 연결 시간 초과의 근본 원인은 동일한 tcp 연결이 사용되도록 ajax 호출이 빠르게 수행되는 경우입니다. HttpResonseMessage를 반환하거나 브라우저 아약스 호출로 반환 된 HTTPResponseException을 발생시켜 401 오류 에테르를 발생 시켰습니다. 그러나 그 호출과 함께 MS는 Startup.Auth.vb 앱에서 User Not Found 오류를 반환했습니다 .UserCookieAuthentication이 활성화되어 응답을 가로 채고 리디렉션을 추가하려고 시도했지만 Object의 Instance가 아닌 Object에 오류가 발생했습니다. 이 오류는 html이지만 사실 이후 응답에 추가되었으므로 ajax 호출이 충분히 빠르게 이루어졌고 동일한 tcp 연결이 브라우저로 리턴되어 다음 호출 앞에 추가 된 경우에만 가능합니다. 어떤 이유로 Chrome이 시간 초과되었습니다. 피들러는 json과 htm의 혼합으로 인해 펑크했지만 파이어 폭스는 실제 오류를 돌 렸습니다. 패킷 스니퍼 또는 파이어 폭스는 이것을 추적하는 유일한 방법입니다.

또한 웹 API 도움말을 사용하여 자동 도움말을 생성하고 HttpResponseMessage를 리턴하는 경우 추가해야합니다.

[System.Web.Http.Description.ResponseType(typeof(CustomReturnedType))] 

도움말이 올바르게 생성되도록 메소드에 속성을 지정하십시오. 그때

return Request.CreateResponse<CustomReturnedType>(objCustomeReturnedType) 

또는 오류

return Request.CreateErrorResponse( System.Net.HttpStatusCode.InternalServerError, new Exception("An Error Ocurred"));

이것은 HttpResponseException을 던진 직후 임의의 시간 초과 또는 서버를 사용할 수없는 누군가를 도울 수 있기를 바랍니다.

또한 HttpResponseException을 반환하면 반환되는 오류가 단일 페이지 앱에서 AuthToken을 새로 고쳐야 할 때 처리되지 않은 예외에서 Visual Studio가 중단되지 않는 이점이 있습니다.

업데이트 : IIS Express 시간 초과에 대한 진술을 철회하고 있습니다. 이는 클라이언트 측의 아약스 콜백에서 실수가되었습니다 .Ajax 1.8이 $ .ajax ()를 반환하고 $ .ajax. (). then () 둘 다 약속을 반환하지만 동일한 체인 약속을 반환하지 않으면 then ()은 실행 순서가 잘못 된 새로운 약속을 반환합니다. 따라서 then () 약속이 완료되면 스크립트 시간 초과였습니다. 키보드와 의자 사이에 IIS Express가 아닌 이상한 문제가 있습니다.


답변

내가 알 수있는 한, 예외를 던지거나 Request.CreateErrorResponse를 반환하는지 여부는 결과가 동일합니다. System.Web.Http.dll의 소스 코드를 보면 많이 볼 수 있습니다. 이 일반적인 요약과 Web Api, HttpError 및 예외의 동작과 비슷한 솔루션을 살펴보십시오.