Tüm yazılara dön
·4 dk okuma

ASP.NET Core'da tutarlı hata yönetimi: IExceptionHandler ve Problem Details

Bir API'nin başarılı yanıtları genellikle özenle tasarlanır. Hata yanıtları ise çoğu zaman tesadüfen oluşur: bir endpoint düz metin döner, diğeri { "error": "..." }, üçüncüsü ise geliştirme ortamındaki yığın izini olduğu gibi istemciye verir. İstemci geliştiricisi her endpoint için ayrı ayrı hata ayrıştırma kodu yazmak zorunda kalır.

Bu yazıda ASP.NET Core'da hataları tek bir yerde yakalamayı, standart Problem Details biçiminde dönmeyi ve bunu yaparken iç ayrıntıları sızdırmamayı anlatıyorum. Amaç, hata davranışını endpoint'lere dağıtmak yerine bir sözleşme olarak ele almak.

Neden tutarlı bir hata biçimi gerekir

İstemci tarafında hata yönetimi üç soruya dayanır: Ne oldu, tekrar denemeli miyim, kullanıcıya ne göstermeliyim? Yanıt biçimi her endpoint'te farklıysa bu sorulara güvenilir cevap vermek mümkün olmaz.

Ortak bir biçim ayrıca sunucu tarafında da işe yarar. Her hata yanıtına aynı izleme kimliği eklenirse, bir kullanıcının bildirdiği hatayı loglarda saniyeler içinde bulabilirsiniz. Tutarlılık hem istemci hem operasyon tarafı için maliyeti düşürür.

Problem Details: standart zarf

HTTP API'leri için hata gövdesini tanımlayan standart RFC 9457'dir (daha önce RFC 7807). Temel alanları şunlardır:

  • type: Hata türünü tanımlayan bir URI.
  • title: Türün kısa, insan tarafından okunabilir özeti.
  • status: HTTP durum kodu.
  • detail: Bu özel olayla ilgili açıklama.
  • instance: Olayın kendisini tanımlayan bir değer.

Bunlara istediğiniz ek alanları (extensions) ekleyebilirsiniz. ASP.NET Core bu biçimi ProblemDetails sınıfıyla ve application/problem+json içerik türüyle destekler.

Servisi yapılandırmak

İlk adım, framework'ün ürettiği hata yanıtlarını da Problem Details'e çevirmek:

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = ctx =>
    {
        ctx.ProblemDetails.Extensions["traceId"] =
            Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier;
    };
});

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

UseExceptionHandler işlenmeyen istisnaları yakalar, UseStatusCodePages ise gövdesiz 404 ya da 405 gibi yanıtları Problem Details'e dönüştürür. CustomizeProblemDetails içinde her yanıta traceId eklemek, tek satırla büyük fayda sağlar.

IExceptionHandler ile istisnaları eşlemek

.NET 8 ile gelen IExceptionHandler, istisna eşlemesini özel bir middleware yazmadan, test edilebilir bir sınıfa taşır:

public sealed class ApiExceptionHandler(
    IProblemDetailsService problemDetails,
    ILogger<ApiExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context, Exception exception, CancellationToken ct)
    {
        var (status, title) = exception switch
        {
            ValidationException => (StatusCodes.Status400BadRequest, "Geçersiz istek"),
            NotFoundException   => (StatusCodes.Status404NotFound, "Kaynak bulunamadı"),
            ConflictException   => (StatusCodes.Status409Conflict, "Çakışma"),
            _                   => (StatusCodes.Status500InternalServerError, "Beklenmeyen hata")
        };

        if (status >= 500)
            logger.LogError(exception, "İşlenmeyen istisna");
        else
            logger.LogInformation("İstemci hatası: {Type}", exception.GetType().Name);

        context.Response.StatusCode = status;
        return await problemDetails.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = context,
            Exception = exception,
            ProblemDetails = { Status = status, Title = title }
        });
    }
}

Kaydı builder.Services.AddExceptionHandler<ApiExceptionHandler>(); ile yaparsınız. Birden fazla handler eklenirse kayıt sırasıyla denenir; true dönen ilki işi bitirir. Hiçbiri işlemezse varsayılan davranış devreye girer.

Alan hatalarını ayrı ele almak

Doğrulama hatalarında istemcinin hangi alanın neden reddedildiğini bilmesi gerekir. Burada ValidationProblemDetails kullanılır:

return Results.ValidationProblem(new Dictionary<string, string[]>
{
    ["email"] = ["Geçerli bir e-posta adresi girin."],
    ["quantity"] = ["Miktar en az 1 olmalı."]
});

Çıktıdaki errors alanı alan adına göre gruplanır. İstemci bu yapıyı form alanlarına doğrudan bağlayabilir. Controller tabanlı projelerde [ApiController] özniteliği model doğrulama hatalarını zaten bu biçimde döner.

Alan hatası ile iş kuralı hatasını birbirine karıştırmayın. "E-posta geçersiz" bir alan hatasıdır. "Bu sipariş artık iptal edilemez" bir iş kuralı hatasıdır; bunu 409 ya da 422 ile, makine tarafından okunabilir bir type değeriyle döndürmek daha doğrudur.

Sızıntıyı önlemek

Hata yanıtı bir saldırgana bilgi vermemelidir. Birkaç kural:

  • Üretimde detail alanına istisna mesajını koymayın. Veri tabanı bağlantı dizesi ya da SQL parçası taşıyabilir.
  • Yığın izi yalnızca loglarda kalsın; gerekirse geliştirme ortamında yanıta eklensin (Exception bağlamı bunun için var).
  • 500 yanıtında genel bir başlık verin ve ayrıntıyı traceId üzerinden loglarda arayın.
  • 404 ile 403 arasındaki ayrımı, kaynağın varlığını ele vermeyecek şekilde düşünün.

Özel istisna türlerinin mesajlarını kullanıcıya gösterilebilir kabul ediyorsanız bunu bilinçli yapın: yalnızca kendi tanımladığınız ValidationException gibi türlerin mesajını detail alanına koyun, geri kalanı genel tutun.

Hata yanıtlarını test etmek

Hata sözleşmesi test edilmezse zamanla bozulur. WebApplicationFactory ile bir istek gönderip durum kodunu, içerik türünü ve alanları doğrulayın:

var response = await client.GetAsync("/orders/yok");

Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("application/problem+json", response.Content.Headers.ContentType?.MediaType);

var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
Assert.Equal(404, problem!.Status);
Assert.True(problem.Extensions.ContainsKey("traceId"));

Ayrıca bir endpoint'in bilerek istisna fırlattığı bir test ekleyin ve yanıtta istisna mesajının bulunmadığını doğrulayın. Sızıntı regresyonları çoğunlukla bu tür bir testle yakalanır.

Kapanış

Tutarlı hata yönetimi üç parçadan oluşur: framework hatalarını da kapsayan AddProblemDetails, istisna eşlemesini tek yere toplayan IExceptionHandler ve her yanıta eklenen bir traceId. Bunların üzerine alan hatalarını ValidationProblemDetails ile ayırmak ve üretimde ayrıntıyı yanıt yerine loga yazmak eklenir.

Küçük başlayın: önce AddProblemDetails ve UseExceptionHandler ile mevcut tüm hataları tek biçime çekin, sonra en sık görülen üç istisna türü için handler yazın. İstemci ekibiniz hata biçiminin artık öngörülebilir olduğunu ilk gün fark edecektir.