Global Exception Handling — middleware, ProblemDetails, RFC 7807

~20 daq · .NET Backend · Ilg'or ASP.NET Core

Muammodan boshlaylik

Har bir action try/catch bilan o'ralgan:

[HttpGet("{id:int}")]
public async Task<IActionResult> Ol(int id)
{
    try
    {
        var m = await service.OlAsync(id);
        return Ok(m);
    }
    catch (TopilmadiException) { return NotFound(); }
    catch (Exception ex)
    {
        return StatusCode(500, ex.ToString());     // stack trace mijozga!
    }
}
  • Bir xil kod 40 ta action da takrorlangan
  • Biror joyda catch unutilsa — mijoz tushunarsiz javob oladi
  • ex.ToString() — stack trace, fayl yo'llari va SQL so'rovlari hujumchiga ko'rinadi
  • Har bir endpoint xatoni o'z formatida qaytaradi

Hayotiy o'xshatma

Kasalxonaning qabul bo'limi. Har bir shifokor o'z kabinetida favqulodda holatlar uchun alohida reanimatsiya qurmaydi — muammo bo'lsa, bemor yagona qabul bo'limiga yuboriladi. U yerda standart tartib bor: holat baholanadi, hujjatlashtiriladi (log), qarindoshlarga tushunarli va bir xil shakldagi ma'lumot beriladi (ProblemDetails) — tibbiy tarix tafsilotlarisiz.


ProblemDetails — standart xato formati

RFC 9457 (avval RFC 7807) — HTTP API xatolari uchun standart JSON:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "42 raqamli mahsulot topilmadi",
  "instance": "/api/mahsulotlar/42",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Mijoz har doim bir xil tuzilmani oladi; Content-Type: application/problem+json.

builder.Services.AddProblemDetails();

Shundan keyin NotFound(), BadRequest() va validatsiya xatolari ham ProblemDetails ko'rinishida qaytadi.


Domen exception lari

public abstract class IlovaException(string xabar) : Exception(xabar);

public class TopilmadiException(string resurs, object id)
    : IlovaException(
quot;{id} raqamli {resurs} topilmadi"); public class BiznesQoidaException(string xabar) : IlovaException(xabar); public class ZiddiyatException(string xabar) : IlovaException(xabar);

Servis — HTTP haqida bilmaydi, faqat ma'noli exception tashlaydi:

public async Task<MahsulotDto> OlAsync(int id, CancellationToken ct) =>
    (await db.Mahsulotlar.FindAsync([id], ct))?.ToDto()
    ?? throw new TopilmadiException("mahsulot", id);

public async Task SotAsync(int id, int soni, CancellationToken ct)
{
    var m = await db.Mahsulotlar.FindAsync([id], ct) ?? throw new TopilmadiException("mahsulot", id);
    if (m.Qoldiq < soni) throw new BiznesQoidaException(
quot;Omborda faqat {m.Qoldiq} dona bor"); // ... }

Controller — try/catch siz:

[HttpGet("{id:int}")]
public async Task<MahsulotDto> Ol(int id, CancellationToken ct) => await service.OlAsync(id, ct);

IExceptionHandler (.NET 8+)

using Microsoft.AspNetCore.Diagnostics;

public class IlovaExceptionHandler(
    IProblemDetailsService pds,
    ILogger<IlovaExceptionHandler> log) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
    {
        var (status, title) = ex switch
        {
            TopilmadiException     => (StatusCodes.Status404NotFound, "Topilmadi"),
            BiznesQoidaException   => (StatusCodes.Status422UnprocessableEntity, "Biznes qoidasi buzildi"),
            ZiddiyatException      => (StatusCodes.Status409Conflict, "Ziddiyat"),
            _                      => (StatusCodes.Status500InternalServerError, "Server xatosi")
        };

        if (status == 500)
            log.LogError(ex, "Kutilmagan xato: {Path}", ctx.Request.Path);
        else
            log.LogInformation("Domen xatosi {Tur}: {Xabar}", ex.GetType().Name, ex.Message);

        ctx.Response.StatusCode = status;
        return await pds.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = ctx,
            Exception = ex,
            ProblemDetails =
            {
                Status = status,
                Title = title,
                Detail = ex is IlovaException ? ex.Message : "Kutilmagan xato yuz berdi. Keyinroq urinib ko'ring."
            }
        });
    }
}
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<IlovaExceptionHandler>();

var app = builder.Build();

app.UseExceptionHandler();      // pipeline boshida
app.UseStatusCodePages();       // body siz 404/405 larga ham ProblemDetails

Bir nechta handler ro'yxatdan o'tkazish mumkin — ular tartib bilan chaqiriladi, birinchi true qaytargani xatoni "qabul qiladi".

[!WARNING] 500 xatolarda ex.Message ni mijozga qaytarmang — unda ulanish satri, jadval nomlari yoki ichki tafsilotlar bo'lishi mumkin. Mijozga — umumiy xabar va traceId; to'liq ma'lumot — faqat log da. traceId orqali qo'llab-quvvatlash xizmati log dan aniq xatoni topadi.


ProblemDetails ni boyitish

builder.Services.AddProblemDetails(o =>
{
    o.CustomizeProblemDetails = ctx =>
    {
        ctx.ProblemDetails.Instance = 
quot;{ctx.HttpContext.Request.Method} {ctx.HttpContext.Request.Path}"; ctx.ProblemDetails.Extensions["traceId"] = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier; }; });

Development va Production

if (app.Environment.IsDevelopment())
    app.UseDeveloperExceptionPage();     // to'liq stack trace — faqat lokal
else
    app.UseExceptionHandler();

[!TIP] .NET 8+ da Development muhitida UseDeveloperExceptionPage sukut bo'yicha yoqilgan. Production da hech qachon — stack trace hujumchi uchun xarita.


Exception — oqimni boshqarish uchun emas

Kutiladigan holatlar uchun exception qimmat (stack trace yig'iladi). Juda tez-tez bo'ladigan holatlarda (masalan, validatsiya) — natija turi:

public record Natija<T>(T? Qiymat, string? Xato)
{
    public bool Muvaffaqiyatli => Xato is null;
    public static Natija<T> Ok(T q) => new(q, null);
    public static Natija<T> Xatolik(string x) => new(default, x);
}

Amaliy kelishuv: "topilmadi" va biznes qoidalar — exception bilan global handler ga, validatsiya — FluentValidation/ModelState bilan (46–47-darslar).


Tez-tez uchraydigan xatolar

UseExceptionHandler pipeline oxirida — undan oldingi middleware xatolari ushlanmaydi; eng boshiga qo'ying.

Exception ni yutib yuborish — catch { } — xato yo'qoladi, log yo'q.

Har bir controller da try/catch + global handler — ikki xil format; bittasini tanlang.


Xulosa

Global exception handling — try/catch ni har bir action dan olib, bitta joyga yig'ish. ProblemDetails (RFC 9457) — standart xato JSON: type, title, status, detail, instance; AddProblemDetails().

IExceptionHandler (.NET 8+): exception turiga qarab status kodi (404, 409, 422, 500), log va IProblemDetailsService bilan javob; AddExceptionHandler<T>() + UseExceptionHandler(). Servislar — domen exception lari, controller lar — try/catch siz. 500 da ichki tafsilot emas — umumiy xabar va traceId.


Keyingi dars

66-dars: Logging — ILogger, log darajalari, Serilog