ASP.NET Web 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あなたの投稿で言及された2つのパラメーターを取るクラスのコンストラクターオーバーロードを取得していません- HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) すなわちHttpResponseException(string, HttpStatusCode)
RBT

回答:


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

エラー処理プロセスの更新。2つのケースがあります。

  1. 見つからない、またはアクションに渡される無効なパラメーターなどの一般的なエラーの場合は、HttpResponseException処理をすぐに停止するためにを返します。さらに、アクションのモデルエラーについては、モデル状態ディクショナリをRequest.CreateErrorResponse拡張機能に渡し、それをでラップしHttpResponseExceptionます。モデル状態ディクショナリを追加すると、応答本文で送信されるモデルエラーのリストが生成されます。

  2. 上位層で発生するエラー、サーバーエラーの場合、例外をWeb APIアプリにバブルさせます。ここには、例外を調べ、ELMAHでログを記録し、正しいHTTPを設定することを理解しようとするグローバル例外フィルターがありますステータスコードと、関連するわかりやすいエラーメッセージが本文として再びHttpResponseException。クライアントがデフォルトの500内部サーバーエラーを受け取ることを予期していない例外の場合、セキュリティ上の理由により一般的なメッセージが表示されます。

アップデート3

最近、Web API 2を取得した後、一般的なエラーを返信するために、IHttpActionResultインターフェイス、具体的にはSystem.Web.Http.Results、NotFound、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);
    }
}

回答ジーピーをありがとう、それは良い経験なので、すぐに説明を送信したいですか?
cuongle

私が言ったようにそれは本当に例外に依存します。たとえば、ユーザーがWeb APIに無効なパラメーターをエンドポイントに渡すなどの致命的な例外が発生した場合、HttpResponseExceptionを作成して、それを使用するアプリに直接返します。
gdp

質問の例外は検証についての詳細です。stackoverflow.com/ a / 22163675/200442を参照してください。
Daniel Little

1
@DanielLittle質問をもう一度読んでください。私は引用します:「これは単なるサンプルコードであり、検証エラーでもサーバーエラーでも関係ありません。」
gdp

@gdpそれでも実際には検証と例外の2つのコンポーネントがあるため、両方をカバーするのが最善です。
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をスローします。これは、タイプResourceNotFoundExceptionのWeb Api 2.2 ExceptionHandlerでチェックし、「id xxxの製品が見つかりません」を返します。そうすることで、各アクションではなく、一般的にアーキテクチャに固定されます。
Pascal

1
以下のための任意の特定の使用があるreturn Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); との違いは何CreateResponseとはCreateErrorResponse
Zapnologica

10
w3.org/Protocols/rfc2616/rfc2616-sec10.htmlによると、クライアントエラーは400レベルのコードであり、サーバーエラーは500レベルのコードです。そのため、「致命的な」エラーだけでなく、500エラーコードがWeb APIの多くの場合に非常に適切な場合があります。
ジェス2015

2
表示するusing System.Net.Http;にはCreateResponse()拡張メソッドが必要です。
Adam Szabo、2015年

Request.CreateResponse()の使用で気に入らないのは、 "<string xmlns =" schemas.microsoft.com/2003/10/Serialization/">My error here </ string > "。400のステータスが適切な状況では、ApiController.BadRequest(string message)がより適切な「<Error> <Message> My error here </ Message> </ Error>」文字列を返すことがわかりました。しかし、単純なメッセージで500ステータスを返すのに相当するものは見つかりません。
vkelman 2016

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 2015年

36

あなたはHttpResponseExceptionをスローすることができます

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

23

Web 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();

}

CustomAttributeクラスをwebApiConfig.csに登録しますconfig.Filters.Add(new DRFValidationFilters());


4

Manish Jainの答えに基づいて構築します(これは、物事を簡素化するWeb 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コントローラーは、サービス関数の結果に基づいて応答を作成します

1つのオプションは、実質的にすべてのパラメーターをオプションとして設定し、より意味のある応答を返すカスタム検証を実行することです。また、例外がサービスの境界を超えないように注意しています。

    [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でWeb API 2プロジェクトを実行しています。
オフザゴールド

これはエラーを返すための単なるモデルであり、どこにでも存在できます。上記のクラスの新しいインスタンスをコントローラーに返します。しかし、正直に言うと、できる限り組み込みのクラスを使用するようにしています:this.Ok()、CreatedAtRoute()、NotFound()。メソッドのシグネチャはIHttpActionResultです。彼らはNetCoreでこのすべてを変更した場合は知ってはいけない
トーマス・ハグストローム

-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 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.