ASP.NET 웹 API에서 오류를 반환하는 모범 사례


384

고객에게 오류를 반환하는 방법에 대해 우려가 있습니다.

오류가 발생 하면 HttpResponseException을 발생시켜 즉시 오류를 반환합니까?

public void Post(Customer customer)
{
    if (string.IsNullOrEmpty(customer.Name))
    {
        throw new HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) 
    }
    if (customer.Accounts.Count == 0)
    {
         throw new HttpResponseException("Customer does not have any account", HttpStatusCode.BadRequest) 
    }
}

또는 모든 오류를 누적 한 다음 클라이언트로 다시 보냅니다.

public void Post(Customer customer)
{
    List<string> errors = new List<string>();
    if (string.IsNullOrEmpty(customer.Name))
    {
        errors.Add("Customer Name cannot be empty"); 
    }
    if (customer.Accounts.Count == 0)
    {
         errors.Add("Customer does not have any account"); 
    }
    var responseMessage = new HttpResponseMessage<List<string>>(errors, HttpStatusCode.BadRequest);
    throw new HttpResponseException(responseMessage);
}

이것은 샘플 코드 일 뿐이며 유효성 검사 오류 또는 서버 오류는 중요하지 않습니다. 각 방법의 우수 사례, 장단점을 알고 싶습니다.


1
stackoverflow.com/a/22163675/200442 를 참조하십시오 ModelState.
Daniel Little

1
여기서 답변은 컨트롤러 자체에서 발생하는 예외 만 다룹니다. API가 아직 실행되지 않은 IQueryable <Model>을 반환하면 예외는 컨트롤러에없고 포착되지 않습니다 ...
Jess

3
아주 좋은 질문이지만 어떻게 든의 생성자 과부하받지 못했습니다 HttpResponseException게시물에 언급 한 두 개의 매개 변수가 필요 클래스 - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest)HttpResponseException(string, HttpStatusCode)
통화 연결음

답변:


293

나를 위해 나는 보통을 돌려 보내고 HttpResponseException예외에 따라 상태 코드를 설정하고 예외가 치명적인지 여부는 HttpResponseException즉시 다시 보낼지 여부를 결정할 것 입니다.

하루가 끝나면 응답이 아닌 뷰를 다시 보내는 API이므로 예외 및 상태 코드가있는 메시지를 소비자에게 다시 보내는 것이 좋습니다. 현재 대부분의 예외는 일반적으로 잘못된 매개 변수 또는 호출 등으로 인해 오류를 누적하고 다시 보낼 필요가 없습니다.

내 응용 프로그램의 예는 때때로 클라이언트가 데이터를 요구하지만 사용 가능한 데이터가 없으므로 사용자 정의를 던진다는 것입니다 NoDataAvailableException Web API 앱으로 버블 링하도록 한 다음 사용자 정의 필터에서 올바른 상태 코드와 함께 관련 메시지가 표시됩니다.

나는 이것을 위해 최선의 방법이 무엇인지 100 % 확신하지 못하지만, 이것은 현재 나를 위해 일하고 있기 때문에 내가하고있는 일입니다.

업데이트 :

이 질문에 대답 한 이후 몇 가지 블로그 게시물이 주제에 대해 작성되었습니다.

https://weblogs.asp.net/fredriknormen/asp-net-web-api-exception-handling

(이것은 야간 빌드에 새로운 기능이 있습니다) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

업데이트 2

오류 처리 프로세스를 업데이트하면 다음 두 가지 경우가 있습니다.

  1. 찾을 수 없거나 잘못된 매개 변수가 조치로 전달되는 것과 같은 일반적인 오류의 경우 HttpResponseException처리를 즉시 중지 하기 위해 a 를 리턴 합니다. 또한 액션에서 모델 오류가 발생하면 모델 상태 사전을 Request.CreateErrorResponse확장 프로그램으로 전달하여에 래핑합니다 HttpResponseException. 모델 상태 사전을 추가하면 응답 본문에 전송 된 모델 오류 목록이 생성됩니다.

  2. 상위 계층, 서버 오류에서 발생하는 오류의 경우 예외를 Web API 앱에 적용합니다. 여기서 예외를보고 ELMAH로 기록하고 올바른 HTTP 설정을 이해하려는 전역 예외 필터가 있습니다. 에서 상태 코드 및 관련 오류 메시지를 다시 본문으로 표시합니다 HttpResponseException. 예외로 인해 클라이언트가 기본 500 내부 서버 오류를받지는 않지만 보안상의 이유로 일반 메시지가 표시 될 것으로 예상됩니다.

업데이트 3

최근에 Web API 2를 가져온 후 일반 오류를 다시 전송하기 위해 IHttpActionResult 인터페이스, 특히 System.Web.Http.ResultsNotFound, BadRequest와 같은 네임 스페이스 에 내장 클래스 ( 예 : 확장하지 않은 경우)를 사용합니다. 응답 메시지가있는 NotFound 결과 :

public class NotFoundWithMessageResult : IHttpActionResult
{
    private string message;

    public NotFoundWithMessageResult(string message)
    {
        this.message = message;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.NotFound);
        response.Content = new StringContent(message);
        return Task.FromResult(response);
    }
}

답변 geepie에 감사드립니다. 좋은 경험이므로 즉시 expcetion을 보내시겠습니까?
cuongle

내가 말했듯이 그것은 실제로 예외에 달려 있습니다. 예를 들어 사용자와 같은 치명적인 예외는 Web Api에 유효하지 않은 매개 변수를 끝점에 전달한 다음 HttpResponseException을 만들어 소비 앱에 바로 반환합니다.
gdp

문제의 예외는 실제로 유효성 검사에 대한 자세한 내용은 stackoverflow.com/a/22163675/200442를 참조하십시오 .
Daniel Little

1
@DanielLittle 그의 질문을 다시 읽으십시오. 인용 : "이것은 샘플 코드 일뿐입니다. 유효성 검사 오류나 서버 오류는 중요하지 않습니다"
gdp

@gdp 그럼에도 불구하고 실제로 유효성 검사와 예외라는 두 가지 구성 요소가 있으므로 두 가지를 모두 다루는 것이 가장 좋습니다.
Daniel Little

184

ASP.NET Web API 2는이를 단순화했습니다. 예를 들어, 다음 코드는

public HttpResponseMessage GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        HttpError err = new HttpError(message);
        return Request.CreateResponse(HttpStatusCode.NotFound, err);
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.OK, item);
    }
}

항목을 찾을 수 없을 때 브라우저에 다음 내용을 반환합니다.

HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: Thu, 09 Aug 2012 23:27:18 GMT
Content-Length: 51

{
  "Message": "Product with id = 12 not found"
}

제안 : 치명적인 오류 (예 : WCF 오류 예외)가 없으면 HTTP 오류 500을 발생시키지 마십시오. 데이터 상태를 나타내는 적절한 HTTP 상태 코드를 선택하십시오. (아래의 apigee 링크를 참조하십시오.)

연결:


4
한 단계 더 나아가 DAL / Repo에서 ResourceNotFoundException을 던져서 WebApi 2.2 ExceptionHandler에서 ResourceNotFoundException 유형을 확인한 다음 "ID가없는 제품을 찾을 수 없음"을 반환합니다. 그렇게하면 일반적으로 각 작업 대신 아키텍처에 고정됩니다.
파스칼

1
에 대한 특정 사용 거기에 return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); 차이점은 무엇 CreateResponse과이CreateErrorResponse
Zapnologica

10
w3.org/Protocols/rfc2616/rfc2616-sec10.html 에 따르면 클라이언트 오류는 400 레벨 코드이고 서버 오류는 500 레벨 코드입니다. 따라서 500 오류 코드는 많은 경우에 단지 "치명적인"오류뿐만 아니라 웹 API에 매우 적합 할 수 있습니다.
Jess

2
당신은 필요 using System.Net.Http;에 대한 CreateResponse()확장 방법을 표시 할 수 있습니다.
Adam Szabo

Request.CreateResponse () 사용에 대해 마음에 들지 않는 점은 "<string xmlns =" schemas.microsoft.com/2003/10/Serialization / "> 내 오류가 여기에 있습니다 </ string 과 같은 불필요한 Microsoft 특정 직렬화 정보를 반환한다는 것입니다. > ". 400 상태가 적절한 상황에서 ApiController.BadRequest (string message)가 더 나은 "<Error> <Message> My error here </ Message> </ Error>"문자열을 반환한다는 것을 알았습니다. 그러나 간단한 메시지로 500 상태를 반환하는 것과 동등한 것을 찾을 수 없습니다.
vkelman

76

오류 / 예외보다 유효성 검사에 더 많은 문제가있는 것 같습니다. 둘 다에 대해 조금 말씀 드리겠습니다.

확인

컨트롤러 작업은 일반적으로 모델에서 직접 유효성 검사가 선언 된 입력 모델을 가져와야합니다.

public class Customer
{ 
    [Require]
    public string Name { get; set; }
}

그런 다음 ActionFilter자동으로 유효성 검사 메시지를 클라이언트로 다시 보내는를 사용할 수 있습니다 .

public class ValidationActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var modelState = actionContext.ModelState;

        if (!modelState.IsValid) {
            actionContext.Response = actionContext.Request
                 .CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
        }
    }
} 

이에 대한 자세한 내용은 http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc를 확인하십시오.

오류 처리

발생한 예외 (관련 상태 코드 포함)를 나타내는 메시지를 클라이언트에 다시 보내는 것이 가장 좋습니다.

Request.CreateErrorResponse(HttpStatusCode, message)메시지를 지정 하려면 즉시 사용해야 합니다. 그러나 이것은 코드를 Request객체에 연결하므로 필요하지 않습니다.

나는 일반적으로 클라이언트가 일반적인 500 오류로 다른 모든 것을 처리하고 포장하는 방법을 알 것으로 기대하는 "안전한"예외 유형을 직접 만듭니다.

액션 필터를 사용하여 예외를 처리하는 방법은 다음과 같습니다.

public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        var exception = context.Exception as ApiException;
        if (exception != null) {
            context.Response = context.Request.CreateErrorResponse(exception.StatusCode, exception.Message);
        }
    }
}

그런 다음 전역으로 등록 할 수 있습니다.

GlobalConfiguration.Configuration.Filters.Add(new ApiExceptionFilterAttribute());

이것이 내 맞춤 예외 유형입니다.

using System;
using System.Net;

namespace WebApi
{
    public class ApiException : Exception
    {
        private readonly HttpStatusCode statusCode;

        public ApiException (HttpStatusCode statusCode, string message, Exception ex)
            : base(message, ex)
        {
            this.statusCode = statusCode;
        }

        public ApiException (HttpStatusCode statusCode, string message)
            : base(message)
        {
            this.statusCode = statusCode;
        }

        public ApiException (HttpStatusCode statusCode)
        {
            this.statusCode = statusCode;
        }

        public HttpStatusCode StatusCode
        {
            get { return this.statusCode; }
        }
    }
}

내 API에서 발생할 수있는 예외 예입니다.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}

ApiExceptionFilterAttribute 클래스 정의에서 오류 처리 답변과 관련된 문제가 있습니다. OnException 메서드에서 예외는 WebException이므로 exception.StatusCode가 유효하지 않습니다. 이 경우 어떻게해야합니까?
razp26

1
@ razp26 오타와 같은 var exception = context.Exception as WebException;것을 언급하고 있다면, 그랬을 것 입니다ApiException
Daniel Little

2
ApiExceptionFilterAttribute 클래스 사용 방법에 대한 예제를 추가 할 수 있습니까?
razp26

36

HttpResponseException을 던질 수 있습니다

HttpResponseMessage response = 
    this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);

23

웹 API 2의 경우 내 메소드는 지속적으로 IHttpActionResult를 반환하므로 다음을 사용합니다.

public IHttpActionResult Save(MyEntity entity)
{
  ....

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}

이 답변은 괜찮습니다.System.Net.Http
Bellash

19

ASP.NET Web API 2를 사용하는 경우 가장 쉬운 방법은 ApiController Short-Method를 사용하는 것입니다. 이로 인해 BadRequestResult가 발생합니다.

return BadRequest("message");

: 엄밀히 모델 유효성 검사 오류가 나는 ModelState 개체를 받아 BadRequest ()의 오버로드를 사용return BadRequest(ModelState);
timmi4sa

4

Web Api에서 사용자 정의 ActionFilter를 사용하여 모델을 검증 할 수 있습니다.

public class DRFValidationFilters : ActionFilterAttribute
{

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request
                 .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);

            //BadRequest(actionContext.ModelState);
        }
    }
    public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {

        return Task.Factory.StartNew(() => {

            if (!actionContext.ModelState.IsValid)
            {
                actionContext.Response = actionContext.Request
                     .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);                    
            }
        });

    }

public class AspirantModel
{
    public int AspirantId { get; set; }
    public string FirstName { get; set; }
    public string MiddleName { get; set; }        
    public string LastName { get; set; }
    public string AspirantType { get; set; }       
    [RegularExpression(@"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$", ErrorMessage = "Not a valid Phone number")]
    public string MobileNumber { get; set; }
    public int StateId { get; set; }
    public int CityId { get; set; }
    public int CenterId { get; set; }

}

    [HttpPost]
    [Route("AspirantCreate")]
    [DRFValidationFilters]
    public IHttpActionResult Create(AspirantModel aspirant)
    {
            if (aspirant != null)
            {

            }
            else
            {
                return Conflict();
            }
          return Ok();

}

webApiConfig.cs에 CustomAttribute 클래스를 등록하십시오. config.Filters.Add (new DRFValidationFilters ());


4

에 구축 Manish Jain(일을 간단하게 웹 API 2 의미)의 대답 :

1) 검증 구조 를 사용 하여 가능한 많은 검증 오류에 응답하십시오. 이러한 구조는 양식에서 오는 요청에 응답하는 데 사용될 수도 있습니다.

public class FieldError
{
    public String FieldName { get; set; }
    public String FieldMessage { get; set; }
}

// a result will be able to inform API client about some general error/information and details information (related to invalid parameter values etc.)
public class ValidationResult<T>
{
    public bool IsError { get; set; }

    /// <summary>
    /// validation message. It is used as a success message if IsError is false, otherwise it is an error message
    /// </summary>
    public string Message { get; set; } = string.Empty;

    public List<FieldError> FieldErrors { get; set; } = new List<FieldError>();

    public T Payload { get; set; }

    public void AddFieldError(string fieldName, string fieldMessage)
    {
        if (string.IsNullOrWhiteSpace(fieldName))
            throw new ArgumentException("Empty field name");

        if (string.IsNullOrWhiteSpace(fieldMessage))
            throw new ArgumentException("Empty field message");

        // appending error to existing one, if field already contains a message
        var existingFieldError = FieldErrors.FirstOrDefault(e => e.FieldName.Equals(fieldName));
        if (existingFieldError == null)
            FieldErrors.Add(new FieldError {FieldName = fieldName, FieldMessage = fieldMessage});
        else
            existingFieldError.FieldMessage = $"{existingFieldError.FieldMessage}. {fieldMessage}";

        IsError = true;
    }

    public void AddEmptyFieldError(string fieldName, string contextInfo = null)
    {
        AddFieldError(fieldName, $"No value provided for field. Context info: {contextInfo}");
    }
}

public class ValidationResult : ValidationResult<object>
{

}

2) 서비스 계층ValidationResult작업의 성공 여부에 관계없이 s 를 반환 합니다. 예 :

    public ValidationResult DoSomeAction(RequestFilters filters)
    {
        var ret = new ValidationResult();

        if (filters.SomeProp1 == null) ret.AddEmptyFieldError(nameof(filters.SomeProp1));
        if (filters.SomeOtherProp2 == null) ret.AddFieldError(nameof(filters.SomeOtherProp2 ), $"Failed to parse {filters.SomeOtherProp2} into integer list");

        if (filters.MinProp == null) ret.AddEmptyFieldError(nameof(filters.MinProp));
        if (filters.MaxProp == null) ret.AddEmptyFieldError(nameof(filters.MaxProp));


        // validation affecting multiple input parameters
        if (filters.MinProp > filters.MaxProp)
        {
            ret.AddFieldError(nameof(filters.MinProp, "Min prop cannot be greater than max prop"));
            ret.AddFieldError(nameof(filters.MaxProp, "Check"));
        }

        // also specify a global error message, if we have at least one error
        if (ret.IsError)
        {
            ret.Message = "Failed to perform DoSomeAction";
            return ret;
        }

        ret.Message = "Successfully performed DoSomeAction";
        return ret;
    }

3) API Controller 는 서비스 기능 결과를 기반으로 응답을 구성합니다.

하나의 옵션은 사실상 모든 매개 변수를 선택 사항으로 설정하고보다 의미있는 응답을 리턴하는 사용자 정의 유효성 검증을 수행하는 것입니다. 또한 예외가 서비스 경계를 ​​벗어나지 않도록주의하고 있습니다.

    [Route("DoSomeAction")]
    [HttpPost]
    public HttpResponseMessage DoSomeAction(int? someProp1 = null, string someOtherProp2 = null, int? minProp = null, int? maxProp = null)
    {
        try
        {
            var filters = new RequestFilters 
            {
                SomeProp1 = someProp1 ,
                SomeOtherProp2 = someOtherProp2.TrySplitIntegerList() ,
                MinProp = minProp, 
                MaxProp = maxProp
            };

            var result = theService.DoSomeAction(filters);
            return !result.IsError ? Request.CreateResponse(HttpStatusCode.OK, result) : Request.CreateResponse(HttpStatusCode.BadRequest, result);
        }
        catch (Exception exc)
        {
            Logger.Log(LogLevel.Error, exc, "Failed to DoSomeAction");
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new HttpError("Failed to DoSomeAction - internal error"));
        }
    }

3

내장 된 "InternalServerError"메소드를 사용하십시오 (ApiController에서 사용 가능).

return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));

0

ASP.NET WebAPI의 현재 상태를 업데이트하기 만하면됩니다. 이제 인터페이스가 호출 IActionResult되고 구현이 크게 변경되지 않았습니다.

[JsonObject(IsReference = true)]
public class DuplicateEntityException : IActionResult
{        
    public DuplicateEntityException(object duplicateEntity, object entityId)
    {
        this.EntityType = duplicateEntity.GetType().Name;
        this.EntityId = entityId;
    }

    /// <summary>
    ///     Id of the duplicate (new) entity
    /// </summary>
    public object EntityId { get; set; }

    /// <summary>
    ///     Type of the duplicate (new) entity
    /// </summary>
    public string EntityType { get; set; }

    public Task ExecuteResultAsync(ActionContext context)
    {
        var message = new StringContent($"{this.EntityType ?? "Entity"} with id {this.EntityId ?? "(no id)"} already exist in the database");

        var response = new HttpResponseMessage(HttpStatusCode.Ambiguous) { Content = message };

        return Task.FromResult(response);
    }

    #endregion
}

이것은 흥미로워 보이지만 프로젝트에서이 코드가 어디에 위치합니까? vb.net에서 웹 API 2 프로젝트를 수행하고 있습니다.
오프 더 골드

오류를 반환하기위한 모델 일 뿐이며 어디서나 상주 할 수 있습니다. 컨트롤러에서 위 클래스의 새 인스턴스를 반환합니다. 그러나 솔직히 말하면 가능한 한 내장 클래스를 사용하려고합니다 : this.Ok (), CreatedAtRoute (), NotFound (). 메소드의 서명은 IHttpActionResult입니다. 그들이 NetCore로이 모든 것을 바꿨는지 모르겠다
Thomas Hagström

-2

modelstate.isvalid가 false 인 오류의 경우 일반적으로 코드에서 발생하는 오류를 보냅니다. 내 서비스를 사용하는 개발자에게는 이해하기 쉽습니다. 나는 일반적으로 아래 코드를 사용하여 결과를 보냅니다.

     if(!ModelState.IsValid) {
                List<string> errorlist=new List<string>();
                foreach (var value in ModelState.Values)
                {
                    foreach(var error in value.Errors)
                    errorlist.Add( error.Exception.ToString());
                    //errorlist.Add(value.Errors);
                }
                HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest,errorlist);}

기본적으로 오류 목록 인 아래 형식으로 클라이언트에 오류를 보냅니다.

    [  
    "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: abc. Path 'Country',** line 6, position 16.\r\n   
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n   
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)",

       "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: ab. Path 'State'**, line 7, position 13.\r\n   
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n   
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n   
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
    ]

외부 API 인 경우 (예 : 공개 인터넷에 노출 된 경우) 예외에서이 수준의 세부 정보를 다시 보내지 않는 것이 좋습니다. 필터에서 더 많은 작업을 수행하고 ToString의 예외가 아니라 오류를 자세히 설명하는 JSON 객체 (또는 선택한 형식 인 경우 XML)를 다시 보내야합니다.
Sudhanshu Mishra

외부 API에 대해이 예외를 보내지 않았습니다. 그러나이를 사용하여 내부 API 및 테스트 중에 문제를 디버깅 할 수 있습니다.
Ashish Sahu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.