Bagikan melalui


Penanganan Kesalahan Global di ASP.NET Web API 2

oleh David Matson, Rick Anderson

Topik ini memberikan gambaran umum tentang penanganan kesalahan global di ASP.NET Web API 2 untuk ASP.NET 4.x. Saat ini tidak ada cara mudah di Web API untuk mencatat atau menangani kesalahan secara global. Beberapa pengecualian yang tidak tertangani dapat diproses melalui filter pengecualian, tetapi ada sejumlah kasus yang tidak dapat ditangani oleh filter pengecualian. Contohnya:

  1. Pengecualian dari konstruktor pengontrol.
  2. Pengecualian dari pengelola pesan.
  3. Pengecualian saat perutean.
  4. Pengecualian selama serialisasi konten respons.

Kami ingin memberikan cara yang sederhana dan konsisten untuk mencatat dan menangani (jika memungkinkan) pengecualian ini.

Ada dua kasus utama untuk menangani pengecualian, kasus di mana kita dapat mengirim respons kesalahan dan kasus di mana yang dapat kita lakukan adalah mencatat pengecualian. Contoh untuk kasus terakhir adalah ketika pengecualian dilemparkan di tengah konten respons streaming; dalam hal ini sudah terlambat untuk mengirim pesan respons baru karena kode status, header, dan konten parsial telah melintasi kawat, jadi kami hanya membatalkan koneksi. Meskipun pengecualian tidak dapat ditangani untuk menghasilkan pesan respons baru, kami masih mendukung pengelogan pengecualian. Dalam kasus di mana kita dapat mendeteksi kesalahan, kita dapat mengembalikan respons kesalahan yang sesuai seperti yang ditunjukkan dalam hal berikut:

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

Opsi yang Ada

Selain filter pengecualian, penangan pesan dapat digunakan hari ini untuk mengamati semua respons tingkat 500, tetapi bertindak pada respons tersebut sulit, karena tidak memiliki konteks tentang kesalahan asli. Penangan pesan juga memiliki beberapa batasan yang sama dengan filter pengecualian mengenai kasus yang dapat mereka tangani. Meskipun Web API memang memiliki infrastruktur pelacakan yang menangkap kondisi kesalahan, infrastruktur pelacakan adalah untuk tujuan diagnostik dan tidak dirancang atau cocok untuk berjalan di lingkungan produksi. Penanganan dan pengelogan pengecualian global harus berupa layanan yang dapat berjalan selama produksi dan dicolokkan ke solusi pemantauan yang ada (misalnya, ELMAH).

Gambaran Umum Solusi

Kami menyediakan dua layanan baru yang dapat diganti pengguna, IExceptionLogger dan IExceptionHandler, untuk mencatat dan menangani pengecualian yang tidak tertangani. Layanan ini sangat mirip, dengan dua perbedaan utama:

  1. Kami mendukung pendaftaran beberapa pencatat pengecualian tetapi hanya satu penangan pengecualian.
  2. Pencatat pengecualian selalu dipanggil, bahkan jika kita akan membatalkan koneksi. Penangan pengecualian hanya dipanggil ketika kita masih dapat memilih pesan respons mana yang akan dikirim.

Kedua layanan menyediakan akses ke konteks pengecualian yang berisi informasi yang relevan dari titik di mana pengecualian terdeteksi, terutama HttpRequestMessage, HttpRequestContext, pengecualian yang dilemparkan, dan sumber pengecualian (detail di bawah).

Prinsip Desain

  1. Tidak ada perubahan yang melanggar Karena fungsionalitas ini ditambahkan dalam rilis kecil, satu batasan penting yang berdampak pada solusi adalah bahwa tidak ada perubahan yang melanggar, baik untuk mengetik kontrak atau perilaku. Batasan ini mengesampingkan beberapa pembersihan yang ingin kami lakukan dalam hal blok tangkapan yang ada mengubah pengecualian menjadi 500 respons. Pembersihan tambahan ini adalah sesuatu yang mungkin kami pertimbangkan untuk rilis utama berikutnya.
  2. Mempertahankan konsistensi dengan konstruksi API Web Alur filter WEB API adalah cara yang bagus untuk menangani masalah lintas pemotongan dengan fleksibilitas penerapan logika pada cakupan khusus tindakan, khusus pengontrol, atau global. Filter, termasuk filter pengecualian, selalu memiliki konteks tindakan dan pengontrol, bahkan ketika terdaftar di cakupan global. Kontrak itu masuk akal untuk filter, tetapi itu berarti bahwa filter pengecualian, bahkan yang terlingkup secara global, tidak cocok untuk beberapa kasus penanganan pengecualian, seperti pengecualian dari penangan pesan, di mana tidak ada tindakan atau konteks pengontrol. Jika kita ingin menggunakan pencakupan fleksibel yang diberikan oleh filter untuk penanganan pengecualian, kita masih memerlukan filter pengecualian. Tetapi jika kita perlu menangani pengecualian di luar konteks pengontrol, kita juga memerlukan konstruksi terpisah untuk penanganan kesalahan global penuh (sesuatu tanpa konteks pengontrol dan batasan konteks tindakan).

Kapan Digunakan

  • Pencatat pengecualian adalah solusi untuk melihat semua pengecualian yang tidak tertangani yang ditangkap oleh Web API.
  • Penangan pengecualian adalah solusi untuk menyesuaikan semua kemungkinan respons terhadap pengecualian yang tidak tertangani yang ditangkap oleh Web API.
  • Filter pengecualian adalah solusi termampu untuk memproses pengecualian tidak tertangani subset yang terkait dengan tindakan atau pengontrol tertentu.

Detail Layanan

Pencatat pengecualian dan antarmuka layanan handler adalah metode asinkron sederhana yang mengambil konteks masing-masing:

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

Kami juga menyediakan kelas dasar untuk kedua antarmuka ini. Mengambil alih metode inti (sinkronisasi atau asinkron) adalah semua yang diperlukan untuk mencatat atau menangani pada waktu yang disarankan. Untuk pengelogan ExceptionLogger , kelas dasar akan memastikan bahwa metode pengelogan inti hanya dipanggil sekali untuk setiap pengecualian (bahkan jika nanti menyebar lebih jauh ke tumpukan panggilan dan ditangkap lagi). Kelas ExceptionHandler dasar akan memanggil metode penanganan inti hanya untuk pengecualian di bagian atas tumpukan panggilan, mengabaikan blok tangkapan berlapis warisan. (Versi sederhana dari kelas dasar ini ada di lampiran di bawah ini.) Baik IExceptionLogger dan IExceptionHandler menerima informasi tentang pengecualian melalui ExceptionContext.

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

Ketika kerangka kerja memanggil pencatat pengecualian atau penangan pengecualian, kerangka kerja akan selalu menyediakan Exception dan Request. Kecuali untuk pengujian unit, itu juga akan selalu menyediakan RequestContext. Ini akan jarang menyediakan ControllerContext dan ActionContext (hanya ketika memanggil dari blok tangkapan untuk filter pengecualian). Ini akan sangat jarang memberikan Response(hanya dalam kasus IIS tertentu ketika di tengah-tengah mencoba menulis respons). Perhatikan bahwa karena beberapa properti ini mungkin null terserah konsumen untuk memeriksa null sebelum mengakses anggota kelas pengecualian.CatchBlock adalah string yang menunjukkan blok tangkapan mana yang melihat pengecualian. String blok tangkapan adalah sebagai berikut:

  • HttpServer (metode SendAsync)

  • HttpControllerDispatcher (metode SendAsync)

  • HttpBatchHandler (metode SendAsync)

  • IExceptionFilter (pemrosesan ApiController dari alur filter pengecualian di ExecuteAsync)

  • Host OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (untuk output buffering)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (untuk output streaming)
  • Host web:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (untuk output buffering)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (untuk output streaming)
    • HttpControllerHandler.WriteErrorResponseContentAsync (untuk kegagalan dalam pemulihan kesalahan dalam mode output buffer)

Daftar string blok tangkapan juga tersedia melalui properti readonly statis. (String blok tangkapan inti ada di ExceptionCatchBlocks statis; sisanya muncul pada satu kelas statis masing-masing untuk OWIN dan host web).IsTopLevelCatchBlock sangat membantu untuk mengikuti pola penanganan pengecualian yang direkomendasikan hanya di bagian atas tumpukan panggilan. Daripada mengubah pengecualian menjadi 500 respons di mana saja blok tangkapan berlapis terjadi, handler pengecualian dapat membiarkan pengecualian menyebar sampai mereka akan dilihat oleh host.

Selain , pencatat ExceptionContextmendapatkan satu informasi lagi melalui :ExceptionLoggerContext

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

Properti kedua, CanBeHandled, memungkinkan pencatat mengidentifikasi pengecualian yang tidak dapat ditangani. Ketika koneksi akan dibatalkan dan tidak ada pesan respons baru yang dapat dikirim, pencatat akan dipanggil tetapi handler tidak akan dipanggil, dan pencatat dapat mengidentifikasi skenario ini dari properti ini.

Selain ExceptionContext, handler mendapatkan satu properti lagi yang dapat diatur secara penuh ExceptionHandlerContext untuk menangani pengecualian:

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

Handler pengecualian menunjukkan bahwa ia telah menangani pengecualian dengan mengatur Result properti ke hasil tindakan (misalnya, ExceptionResult, InternalServerErrorResult, StatusCodeResult, atau hasil kustom). Result Jika properti null, pengecualian tidak tertangani dan pengecualian asli akan dilemparkan kembali.

Untuk pengecualian di bagian atas tumpukan panggilan, kami mengambil langkah tambahan untuk memastikan respons sesuai untuk pemanggil API. Jika pengecualian menyebar hingga host, pemanggil akan melihat layar kuning kematian atau beberapa respons yang disediakan host lain yang biasanya HTML dan biasanya bukan respons kesalahan API yang sesuai. Dalam kasus ini, Hasilnya dimulai non-null, dan hanya jika handler pengecualian kustom secara eksplisit mengaturnya kembali ke null (tidak tertangani) akan pengecualian disebarluaskan ke host. Pengaturan Result ke null dalam kasus seperti itu dapat berguna untuk dua skenario:

  1. OWIN menghosting WEB API dengan middleware penanganan pengecualian kustom yang terdaftar sebelum/di luar WEB API.
  2. Penelusuran kesalahan lokal melalui browser, di mana layar kuning kematian sebenarnya merupakan respons yang bermanfaat untuk pengecualian yang tidak tertangani.

Untuk pencatat pengecualian dan penangan pengecualian, kami tidak melakukan apa pun untuk memulihkan jika pencatat atau penangan itu sendiri melemparkan pengecualian. (Selain membiarkan pengecualian menyebar, tinggalkan umpan balik di bagian bawah halaman ini jika Anda memiliki pendekatan yang lebih baik.) Kontrak untuk pencatat dan penangan pengecualian adalah bahwa mereka tidak boleh membiarkan pengecualian disebarluaskan ke pemanggil mereka; jika tidak, pengecualian hanya akan menyebar, seringkali sepanjang jalan ke host mengakibatkan kesalahan HTML (seperti ASP. Layar kuning NET) dikirim kembali ke klien (yang biasanya bukan opsi pilihan untuk pemanggil API yang mengharapkan JSON atau XML).

Contoh

Melacak Pencatat Pengecualian

Pencatat pengecualian di bawah ini mengirimkan data pengecualian ke sumber Pelacakan yang dikonfigurasi (termasuk jendela output Debug di Visual Studio).

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

Penangan Pengecualian Pesan Kesalahan Kustom

Handler pengecualian di bawah ini menghasilkan respons kesalahan kustom kepada klien, termasuk alamat email untuk menghubungi dukungan.

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

Mendaftarkan Filter Pengecualian

Jika Anda menggunakan templat proyek "ASP.NET MVC 4 Web Application" untuk membuat proyek Anda, letakkan kode konfigurasi Api Web Anda di dalam WebApiConfig kelas, di folder App_Start :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

Lampiran: Detail Kelas Dasar

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}