ASP.NET Core Web API penanganan pengecualian

280

Saya menggunakan ASP.NET Core untuk proyek REST API baru saya setelah menggunakan ASP.NET Web API biasa selama bertahun-tahun. Saya tidak melihat cara yang baik untuk menangani pengecualian di ASP.NET Core Web API. Saya mencoba menerapkan pengecualian penanganan filter / atribut:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

Dan inilah pendaftaran Startup filter saya:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

Masalah yang saya alami adalah bahwa ketika pengecualian terjadi pada saya AuthorizationFilteritu tidak ditangani olehErrorHandlingFilter . Saya mengharapkan untuk ditangkap di sana seperti itu bekerja dengan ASP.NET Web API lama.

Jadi bagaimana saya bisa menangkap semua pengecualian aplikasi dan juga pengecualian dari Filter Tindakan?

Andrei
sumber
3
Sudahkah Anda mencoba UseExceptionHandlermiddleware?
Pawel
Saya punya contoh di sini tentang cara menggunakan UseExceptionHandlermiddleware
Ilya Chernomordik

Jawaban:

539

Middleware Handling Pengecualian

Setelah banyak percobaan dengan pendekatan penanganan pengecualian yang berbeda, saya akhirnya menggunakan middleware. Itu berhasil yang terbaik untuk aplikasi ASP.NET Core Web API saya. Ini menangani pengecualian aplikasi serta pengecualian dari filter tindakan dan saya memiliki kontrol penuh atas penanganan pengecualian dan respons HTTP. Ini pengecualian saya menangani middleware:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

Daftarkan sebelum MVC di Startupkelas:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Anda dapat menambahkan jejak stack, nama jenis pengecualian, kode kesalahan atau apa pun yang Anda inginkan. Sangat fleksibel. Berikut adalah contoh tanggapan pengecualian:

{ "error": "Authentication token is not valid." }

Pertimbangkan menyuntikkan IOptions<MvcJsonOptions>ke Invokemetode untuk kemudian menggunakannya ketika Anda membuat serial objek respon untuk memanfaatkan pengaturan serialisasi ASP.NET MVC JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)untuk konsistensi serialisasi yang lebih baik di semua titik akhir.

Pendekatan 2

Ada API tidak jelas lainnya yang disebut UseExceptionHandler"ok" untuk skenario sederhana:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Ini bukan cara yang sangat jelas tapi mudah untuk mengatur penanganan pengecualian. Namun saya masih lebih suka pendekatan middleware daripada itu karena saya mendapatkan lebih banyak kontrol dengan kemampuan untuk menyuntikkan dependensi yang diperlukan.

Andrei
sumber
4
Saya telah membenturkan kepala saya ke meja berusaha mendapatkan middleware khusus untuk bekerja hari ini, dan pada dasarnya bekerja dengan cara yang sama (Saya menggunakannya untuk mengelola unit kerja / transaksi untuk permintaan). Masalah yang saya hadapi adalah bahwa pengecualian yang dikemukakan di 'berikutnya' tidak terperangkap dalam middleware. Seperti yang dapat Anda bayangkan, ini bermasalah. Apa yang saya lakukan salah / hilang? Adakah petunjuk atau saran?
brappleye3
5
@ brappleye3 - Saya sudah tahu apa masalahnya. Saya baru saja mendaftarkan middleware di tempat yang salah di kelas Startup.cs. Saya pindah app.UseMiddleware<ErrorHandlingMiddleware>();ke sebelum app.UseStaticFiles();. Pengecualian tampaknya ditangkap dengan benar sekarang. Ini membuat saya percaya bahwa app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();lakukan beberapa peretasan middleware sihir internal untuk mendapatkan pemesanan middleware yang benar.
Jamadan
4
Saya setuju bahwa middleware khusus bisa sangat berguna tetapi akan mempertanyakan menggunakan pengecualian untuk situasi NotFound, Unauthorized, dan BadRequest. Mengapa tidak hanya mengatur kode status (menggunakan NotFound () dll) dan kemudian menanganinya di middleware kustom Anda atau melalui UseStatusCodePagesWithReExecute? Lihat devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api untuk info lebih lanjut
Paul Hiles
4
Itu buruk karena selalu membuat serial ke JSON, sepenuhnya mengabaikan negosiasi konten.
Konrad
5
Titik valid @Konrad. Itu sebabnya saya mengatakan bahwa contoh ini adalah di mana Anda dapat memulai, dan bukan hasil akhirnya. Untuk 99% API, JSON lebih dari cukup. Jika Anda merasa jawaban ini tidak cukup baik, silakan berkontribusi.
Andrei
61

Terbaru Asp.Net Core(setidaknya dari 2.2, mungkin sebelumnya) memiliki middleware built-in yang membuatnya sedikit lebih mudah dibandingkan dengan implementasi dalam jawaban yang diterima:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Seharusnya melakukan hampir sama, hanya sedikit kode untuk menulis.

Penting: Ingatlah untuk menambahkannya sebelumnya UseMvc(atau UseRoutingdalam .Net Core 3) karena urutannya penting.

Ilya Chernomordik
sumber
Apakah ini mendukung DI sebagai arg ke handler, atau apakah seseorang harus menggunakan pola pencari lokasi di dalam handler?
lp
33

Taruhan terbaik Anda adalah menggunakan middleware untuk mencapai pencatatan yang Anda cari. Anda ingin menempatkan pengecualian masuk dalam satu middleware dan kemudian menangani halaman kesalahan yang ditampilkan kepada pengguna di middleware yang berbeda. Itu memungkinkan pemisahan logika dan mengikuti desain yang telah disusun Microsoft dengan 2 komponen middleware. Berikut ini tautan yang bagus ke dokumentasi Microsoft: Penanganan Kesalahan di ASP.Net Core

Untuk contoh spesifik Anda, Anda mungkin ingin menggunakan salah satu ekstensi di middleware StatusCodePage atau roll sendiri seperti ini .

Anda dapat menemukan contoh di sini untuk mencatat pengecualian: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Jika Anda tidak menyukai implementasi spesifik tersebut, maka Anda juga dapat menggunakan ELM Middleware , dan berikut adalah beberapa contohnya: Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Jika itu tidak berhasil untuk kebutuhan Anda, Anda selalu dapat menggulung komponen Middleware Anda sendiri dengan melihat implementasi ExceptionHandlerMiddleware dan ElmMiddleware untuk memahami konsep untuk membangun sendiri.

Sangat penting untuk menambahkan pengecualian penanganan middleware di bawah middleware StatusCodePages tetapi di atas semua komponen middleware lainnya. Dengan begitu middleware Pengecualian Anda akan menangkap pengecualian, catat, lalu izinkan permintaan untuk melanjutkan ke middleware StatusCodePage yang akan menampilkan halaman kesalahan ramah kepada pengguna.

Ashley Lee
sumber
Sama-sama. Saya juga memberikan tautan ke contoh untuk mengganti UseStatusPages default pada kasus tepi yang mungkin lebih baik memenuhi permintaan Anda.
Ashley Lee
1
Perhatikan bahwa Elm tidak menyimpan log, dan disarankan untuk menggunakan Serilog atau NLog untuk memberikan serialisasi. Lihat log ELM menghilang. Bisakah kita bertahan ke file atau DB?
Michael Freidgeim
2
Tautan sekarang rusak.
Mathias Lykkegaard Lorenzen
@ AshleyLee, saya mempertanyakan hal UseStatusCodePagesitu berguna dalam implementasi layanan Web API. Tidak ada tampilan atau HTML sama sekali, hanya tanggapan JSON ...
Paul Michalik
23

Jawaban yang diterima dengan baik banyak membantu saya tetapi saya ingin melewatkan HttpStatusCode di middleware saya untuk mengelola kode status kesalahan saat runtime.

Menurut tautan ini saya mendapat ide untuk melakukan hal yang sama. Jadi saya menggabungkan Jawaban Andrei dengan ini. Jadi kode terakhir saya di bawah ini:
1. Kelas dasar

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Jenis Kelas Pengecualian Kustom

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

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

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Perkecualian Perkecualian Kustom

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Metode Perpanjangan

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. Konfigurasikan Metode di startup.cs

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Sekarang metode login saya di pengontrol Akun:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

Di atas Anda dapat melihat apakah saya belum menemukan pengguna kemudian menaikkan HttpStatusCodeException di mana saya telah melewati HttpStatusCode.NotFound status dan pesan khusus di
middleware

catch (ex HttpStatusCodeException)

Diblokir akan dipanggil yang akan melewati kontrol

private Task HandleExceptionAsync (konteks HttpContext, pengecualian HttpStatusCodeException)

.


Tetapi bagaimana jika saya mendapat kesalahan runtime sebelumnya? Untuk itu saya telah menggunakan try catch block yang melempar exception dan akan ditangkap di catch (Exception exceptionObj) block dan akan melewati kontrol ke

Task HandleExceptionAsync (Konteks HttpContext, Pengecualian pengecualian)

metode.

Saya telah menggunakan kelas ErrorDetails tunggal untuk keseragaman.

Arjun
sumber
Di mana harus meletakkan metode ekstensi? Sayangnya di startup.csdalam void Configure(IapplicationBuilder app)saya mendapatkan kesalahan IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. Dan saya menambahkan referensi, dimana CustomExceptionMiddleware.cs.
Spedo De La Rossa
Anda tidak ingin menggunakan pengecualian karena memperlambat apis Anda. pengecualian sangat mahal.
lnaie
@ Inaie, Tidak bisa mengatakan tentang itu ... tapi sepertinya Anda tidak pernah punya pengecualian untuk ditangani .. Kerja bagus
Arjun
19

Untuk Mengkonfigurasi perilaku penanganan pengecualian per jenis pengecualian, Anda dapat menggunakan Middleware dari paket NuGet:

Contoh kode:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}
Ihar Yakimush
sumber
16

Pertama, terima kasih kepada Andrei karena saya mendasarkan solusi saya pada contohnya.

Saya termasuk milik saya karena sampel yang lebih lengkap dan mungkin menghemat waktu pembaca.

Keterbatasan pendekatan Andrei adalah bahwa tidak menangani pencatatan, menangkap variabel permintaan yang berpotensi bermanfaat dan negosiasi konten (itu akan selalu mengembalikan JSON tidak peduli apa yang klien minta - XML ​​/ teks biasa, dll).

Pendekatan saya adalah menggunakan ObjectResult yang memungkinkan kita untuk menggunakan fungsi yang dipanggang ke dalam MVC.

Kode ini juga mencegah caching dari respons.

Respons kesalahan telah didekorasi sedemikian rupa sehingga dapat diserialisasi oleh serializer XML.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}
CountZero
sumber
9

Pertama, konfigurasikan ASP.NET Core 2 Startupuntuk menjalankan kembali ke halaman kesalahan untuk setiap kesalahan dari server web dan setiap pengecualian yang tidak ditangani.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Selanjutnya, tentukan jenis pengecualian yang akan memungkinkan Anda membuang kesalahan dengan kode status HTTP.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Terakhir, di pengontrol Anda untuk halaman kesalahan, sesuaikan respons berdasarkan alasan kesalahan dan apakah respons akan dilihat langsung oleh pengguna akhir. Kode ini mengasumsikan semua URL API dimulai dengan /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core akan mencatat rincian kesalahan untuk Anda debug, jadi kode status mungkin yang Anda inginkan untuk meminta (mungkin tidak dipercaya) pemohon. Jika Anda ingin menampilkan lebih banyak info, Anda dapat meningkatkan HttpExceptionuntuk memberikannya. Untuk kesalahan API, Anda bisa memasukkan info kesalahan yang dikodekan JSON di badan pesan dengan menggantinya return StatusCode...dengan return Json....

Edward Brey
sumber
0

gunakan middleware atau IExceptionHandlerPathFeature baik-baik saja. ada cara lain di eshop

buat filter pengecualian dan daftarkan

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
ws_
sumber