Praktik terbaik untuk mengembalikan kesalahan di ASP.NET Web API

384

Saya memiliki kekhawatiran tentang cara kami mengembalikan kesalahan ke klien.

Apakah kami segera mengembalikan kesalahan dengan melemparkan HttpResponseException ketika kami mendapatkan kesalahan:

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

Atau kami mengakumulasikan semua kesalahan kemudian mengirim kembali ke klien:

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

Ini hanya kode sampel, tidak masalah kesalahan validasi atau kesalahan server, saya hanya ingin tahu praktik terbaik, pro dan kontra dari setiap pendekatan.

cuongle
sumber
1
Lihat stackoverflow.com/a/22163675/200442 yang harus Anda gunakan ModelState.
Daniel Little
1
Perhatikan bahwa jawaban di sini hanya mencakup Pengecualian yang dilemparkan ke dalam pengontrol itu sendiri. Jika API Anda mengembalikan <Model> IQueryable yang belum dieksekusi, pengecualian tidak di controller dan tidak tertangkap ...
Jess
3
Pertanyaan yang sangat bagus tapi entah mengapa saya tidak mendapatkan kelebihan konstruktor dari HttpResponseExceptionkelas yang mengambil dua parameter yang disebutkan dalam posting Anda - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) yaituHttpResponseException(string, HttpStatusCode)
RBT

Jawaban:

293

Bagi saya, saya biasanya mengirim kembali HttpResponseExceptiondan mengatur kode status sesuai tergantung pada pengecualian yang dilemparkan dan jika pengecualian itu fatal atau tidak akan menentukan apakah saya HttpResponseExceptionsegera mengirim kembali .

Pada akhirnya, ini adalah API yang mengirim kembali respons dan bukan tampilan, jadi saya pikir tidak apa-apa untuk mengirim kembali pesan dengan pengecualian dan kode status ke konsumen. Saat ini saya tidak perlu mengumpulkan kesalahan dan mengirimnya kembali karena sebagian besar pengecualian biasanya karena parameter atau panggilan yang salah, dll

Contoh di aplikasi saya adalah bahwa kadang-kadang klien akan meminta data, tetapi tidak ada data yang tersedia jadi saya melempar kustom NoDataAvailableExceptiondan membiarkannya menggelembung ke aplikasi Web API, di mana kemudian di filter kustom saya yang menangkapnya mengirim kembali pesan yang relevan bersama dengan kode status yang benar.

Saya tidak 100% yakin tentang apa praktik terbaik untuk ini, tetapi ini bekerja untuk saya saat ini sehingga itulah yang saya lakukan.

Perbarui :

Sejak saya menjawab pertanyaan ini, beberapa posting blog telah ditulis pada topik:

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

(Yang ini memiliki beberapa fitur baru di build malam) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Perbarui 2

Pembaruan untuk proses penanganan kesalahan kami, kami memiliki dua kasus:

  1. Untuk kesalahan umum seperti tidak ditemukan, atau parameter tidak valid diteruskan ke tindakan, kami mengembalikan a HttpResponseExceptionuntuk segera menghentikan pemrosesan. Selain itu untuk kesalahan model dalam tindakan kami, kami akan menyerahkan kamus status model ke Request.CreateErrorResponseekstensi dan membungkusnya dengan a HttpResponseException. Menambahkan kamus model keadaan menghasilkan daftar kesalahan model yang dikirim di badan respons.

  2. Untuk kesalahan yang terjadi di lapisan yang lebih tinggi, kesalahan server, kami membiarkan gelembung pengecualian ke aplikasi Web API, di sini kami memiliki filter pengecualian global yang melihat pengecualian, mencatatnya dengan ELMAH dan mencoba memahami pengaturannya pada HTTP yang benar kode status dan pesan kesalahan ramah yang relevan sebagai isi lagi di a HttpResponseException. Untuk pengecualian yang tidak kami harapkan klien akan menerima kesalahan server internal 500 default, tetapi pesan umum karena alasan keamanan.

Perbarui 3

Baru-baru ini, setelah mengambil Web API 2, untuk mengirim kembali kesalahan umum, kami sekarang menggunakan antarmuka IHttpActionResult , khususnya kelas System.Web.Http.Resultsbawaan di dalam namespace seperti NotFound, BadRequest ketika mereka cocok, jika mereka tidak diperluas, misalnya hasil NotFound dengan pesan respons:

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);
    }
}
gdp
sumber
Terima kasih atas jawaban Anda, ini pengalaman yang baik, jadi Anda lebih suka mengirim expcetion segera?
cuongle
Seperti yang saya katakan itu sangat tergantung pada pengecualian. Pengecualian yang fatal seperti misalnya pengguna mengirimkan Api Web parameter yang tidak valid ke titik akhir, maka saya akan membuat HttpResponseException dan mengembalikannya langsung ke aplikasi yang mengkonsumsi.
gdp
Pengecualian dalam pertanyaan ini lebih tentang validasi, lihat stackoverflow.com/a/22163675/200442 .
Daniel Little
1
@DanielLittle Baca kembali pertanyaannya. Saya kutip: "Ini hanya kode contoh, tidak masalah baik kesalahan validasi atau kesalahan server"
gdp
@ gdp Meski begitu sebenarnya ada dua komponen untuk itu, validasi dan pengecualian, jadi yang terbaik adalah mencakup keduanya.
Daniel Little
184

ASP.NET Web API 2 sangat menyederhanakannya. Misalnya, kode berikut:

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

mengembalikan konten berikut ke browser saat item tidak ditemukan:

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

Saran: Jangan melempar Kesalahan HTTP 500 kecuali ada kesalahan katastropik (misalnya, Pengecualian Kesalahan WCF). Pilih kode status HTTP yang sesuai yang mewakili keadaan data Anda. (Lihat tautan apigee di bawah ini.)

Tautan:

Manish Jain
sumber
4
Saya akan melangkah lebih jauh dan melempar ResourceNotFoundException dari DAL / Repo yang saya periksa di Web Api 2.2 ExceptionHandler untuk Type ResourceNotFoundException dan kemudian saya mengembalikan "Produk dengan id xxx tidak ditemukan". Dengan cara itu secara umum berlabuh di arsitektur daripada setiap tindakan.
Pascal
1
Apakah ada penggunaan khusus untuk return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); Apa perbedaan antara CreateResponsedanCreateErrorResponse
Zapnologica
10
Menurut, w3.org/Protocols/rfc2616/rfc2616-sec10.html , kesalahan klien adalah kode level 400 dan kesalahan server adalah kode level 500. Jadi kode kesalahan 500 mungkin sangat tepat dalam banyak kasus untuk Web API, bukan hanya kesalahan "bencana".
Jess
2
Anda perlu using System.Net.Http;agar CreateResponse()metode ekstensi muncul.
Adam Szabo
Apa yang saya tidak suka tentang menggunakan Request.CreateResponse () adalah bahwa ia mengembalikan info serialisasi spesifik Microsoft yang tidak perlu seperti "<string xmlns =" schemas.microsoft.com/2003/10/Serialization/ " > Kesalahan saya di sini </ string > ". Untuk situasi ketika 400 status sesuai, saya menemukan bahwa apiController.BadRequest (pesan string) mengembalikan string "<Error> <Message> Kesalahan saya di sini </Message> </Error>" lebih baik. Tetapi saya tidak dapat menemukan padanannya untuk mengembalikan 500 status dengan pesan sederhana.
vkelman
76

Sepertinya Anda mengalami lebih banyak masalah dengan Validasi daripada kesalahan / pengecualian, jadi saya akan mengatakan sedikit tentang keduanya.

Validasi

Tindakan pengontrol umumnya harus mengambil Model Input di mana validasi dinyatakan langsung pada model.

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

Kemudian Anda dapat menggunakan ActionFilteryang secara otomatis mengirim pesan validasi kembali ke klien.

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

Untuk informasi lebih lanjut tentang ini, periksa http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Menangani kesalahan

Yang terbaik adalah mengembalikan pesan kembali ke klien yang mewakili pengecualian yang terjadi (dengan kode status yang relevan).

Di luar kotak Anda harus menggunakan Request.CreateErrorResponse(HttpStatusCode, message)jika Anda ingin menentukan pesan. Namun, ini mengikat kode ke Requestobjek, yang tidak perlu Anda lakukan.

Saya biasanya membuat jenis "aman" pengecualian saya sendiri yang saya harapkan klien akan tahu bagaimana menangani dan membungkus semua orang lain dengan kesalahan 500 generik.

Menggunakan filter tindakan untuk menangani pengecualian akan terlihat seperti ini:

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

Kemudian Anda dapat mendaftarkannya secara global.

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

Ini adalah jenis pengecualian khusus saya.

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

Contoh pengecualian yang dapat saya lemparkan oleh API.

public class NotAuthenticatedException : ApiException
{
    public NotAuthenticatedException()
        : base(HttpStatusCode.Forbidden)
    {
    }
}
Daniel Little
sumber
Saya memiliki masalah terkait dengan jawaban penanganan kesalahan di definisi kelas ApiExceptionFilterAttribute. Dalam metode OnException, exception.StatusCode tidak valid karena pengecualian adalah WebException. Apa yang bisa saya lakukan dalam kasus ini?
razp26
1
@ razp26 jika Anda mengacu pada seperti var exception = context.Exception as WebException;itu salah ketik, seharusnyaApiException
Daniel Little
2
Bisakah Anda menambahkan contoh bagaimana kelas ApiExceptionFilterAttribute akan digunakan?
razp26
36

Anda dapat melempar HttpResponseException

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

Untuk Web API 2 metode saya secara konsisten mengembalikan IHttpActionResult jadi saya menggunakan ...

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

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}
Mick
sumber
Jawaban ini oke, sementara Anda harus menambahkan referensi keSystem.Net.Http
Bellash
19

Jika Anda menggunakan ASP.NET Web API 2, cara termudah adalah dengan menggunakan Metode Pendek ApiController. Ini akan menghasilkan BadRequestResult.

return BadRequest("message");
Fabian von Ellerts
sumber
Ketat untuk kesalahan validasi model saya menggunakan kelebihan BadRequest () yang menerima objek ModelState:return BadRequest(ModelState);
timmi4sa
4

Anda dapat menggunakan ActionFilter khusus di Web Api untuk memvalidasi model

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

}

Daftarkan kelas CustomAttribute di webApiConfig.cs config.Filters.Add (DRFValidationFilters baru ());

LokeshChikkala
sumber
4

Membangun Manish Jainjawaban (yang dimaksudkan untuk Web API 2 yang menyederhanakan banyak hal):

1) Gunakan struktur validasi untuk merespons sebanyak mungkin kesalahan validasi. Struktur ini juga dapat digunakan untuk menanggapi permintaan yang datang dari formulir.

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) Lapisan layanan akan kembali ValidationResults, terlepas dari operasi yang berhasil atau tidak. Misalnya:

    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) Pengontrol API akan menyusun respons berdasarkan hasil fungsi layanan

Salah satu opsi adalah menempatkan hampir semua parameter sebagai opsional dan melakukan validasi khusus yang mengembalikan respons yang lebih bermakna. Juga, saya berhati-hati untuk tidak membiarkan pengecualian melampaui batas layanan.

    [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"));
        }
    }
Alexei
sumber
3

Gunakan metode "InternalServerError" bawaan (tersedia di ApiController):

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

Hanya untuk memperbarui pada kondisi saat ini ASP.NET WebAPI. Antarmuka sekarang dipanggil IActionResultdan implementasi tidak banyak berubah:

[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
}
Thomas Hagström
sumber
Ini terlihat menarik, tetapi di mana khusus dalam proyek ini kode ini ditempatkan? Saya melakukan proyek web api 2 di vb.net.
Off The Gold
Itu hanya model untuk mengembalikan kesalahan dan dapat berada di mana saja. Anda akan mengembalikan instance baru dari kelas di atas di Controller Anda. Tapi sejujurnya saya mencoba menggunakan kelas built in kapan pun memungkinkan: this.Ok (), CreatedAtRoute (), NotFound (). Tanda tangan metode ini adalah IHttpActionResult. Tidak tahu apakah mereka mengubah semua ini dengan NetCore
Thomas Hagström
-2

Untuk kesalahan di mana modelstate.isvalid salah, saya biasanya mengirim kesalahan karena dilemparkan oleh kode. Mudah dimengerti oleh pengembang yang mengonsumsi layanan saya. Saya biasanya mengirim hasilnya menggunakan kode di bawah ini.

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

Ini mengirimkan kesalahan ke klien dalam format di bawah ini yang pada dasarnya adalah daftar kesalahan:

    [  
    "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)"
    ]
Ashish Sahu
sumber
Saya tidak akan merekomendasikan mengirim kembali tingkat detail ini dengan pengecualian jika ini adalah API eksternal (mis. Terkena internet publik). Anda harus melakukan lebih banyak pekerjaan di filter dan mengirim kembali objek JSON (atau XML jika itu format yang dipilih) merinci kesalahan daripada hanya ToString pengecualian.
Sudhanshu Mishra
Benar tidak mengirim pengecualian ini untuk API eksternal. Tetapi Anda dapat menggunakannya untuk men-debug masalah di API internal dan selama pengujian.
Ashish Sahu