Langsung ke konten
~/rifqi_abrory

Halaman

Beranda
Tentang
Stack
Pengalaman
Proyek
Tulisan
Kontak
Semua proyek

Proyek

Avrist PINTAR — Agent Portal
Avrist e-Application
Avrist Academy — LMS CMS
Avrist e-Payment
PRUHub
SIYAP

Artikel

Angular signals untuk halaman admin sehari-hari
Typed reactive forms di Angular, tanpa any
Error API yang konsisten di ASP.NET Core dengan ProblemDetails
Meneruskan CancellationToken sampai ke lapisan terbawah di ASP.NET Core
Graceful shutdown untuk service HTTP di Go
Idempotency key: membuat API pembayaran aman untuk di-retry
Form kontak yang tahan spam dengan Next.js Server Actions
Form yang ramah offline di React Native: draft dan antrean submit
Validasi request dan penanganan error di Spring Boot 3
Keyset pagination di SQL Server: ketika OFFSET mulai lambat

Aksi

Ganti ke mode gelap
Read in English
Salin alamat email
Semua artikel

Error API yang konsisten di ASP.NET Core dengan ProblemDetails

Satu bentuk error untuk semua kegagalan: validasi, data tidak ditemukan, dan exception yang tidak tertangani, memakai format RFC 9457 bawaan ASP.NET Core.

3 menit baca

Di halaman ini

Masalah error yang dibuat seadanya

Banyak API berawal dengan bentuk error yang berbeda-beda tergantung siapa yang menulis endpoint-nya. Ada yang mengembalikan { "message": "Not found" }, ada yang berupa string biasa, dan exception yang tidak tertangani malah mengembalikan halaman error HTML. Akibatnya setiap client (admin panel Angular, aplikasi mobile, atau service lain) butuh penanganan khusus hanya untuk menampilkan pesan yang masuk akal.

ASP.NET Core sebenarnya sudah punya jawaban standar: ProblemDetails, format application/problem+json dari RFC 9457. Tujuan artikel ini sederhana: semua respons non-2xx dari API punya bentuk yang sama.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "Policy 'P-001' was not found.",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Mengaktifkannya

Cukup dua baris di Program.cs untuk mendapatkan ProblemDetails pada respons status code dan exception:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails(options =>
{
    // Add the trace id to every problem, so support can find the request in the logs.
    options.CustomizeProblemDetails = context =>
        context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
});

builder.Services.AddControllers();

var app = builder.Build();

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

app.MapControllers();
app.Run();
  • UseExceptionHandler() menangkap exception yang tidak tertangani dan menulis ProblemDetails 500, alih-alih membocorkan stack trace.
  • UseStatusCodePages() membuat respons kosong seperti 404 polos juga punya body ProblemDetails.
  • Controller dengan atribut [ApiController] sudah otomatis mengembalikan ValidationProblemDetails (400) ketika model binding atau data annotation gagal.

Memetakan exception milik sendiri

Aturan bisnis biasanya ada di service, bukan di controller. Daripada menulis try/catch di setiap action, lempar exception yang bermakna lalu terjemahkan di satu tempat dengan IExceptionHandler (.NET 8+):

public sealed class NotFoundException(string message) : Exception(message);

public sealed class ConflictException(string message) : Exception(message);

internal sealed class DomainExceptionHandler(IProblemDetailsService problemDetails) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        var status = exception switch
        {
            NotFoundException => StatusCodes.Status404NotFound,
            ConflictException => StatusCodes.Status409Conflict,
            _ => 0,
        };

        // Not ours: let the default handler produce a generic 500.
        if (status == 0) return false;

        httpContext.Response.StatusCode = status;
        return await problemDetails.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            Exception = exception,
            ProblemDetails = { Status = status, Detail = exception.Message },
        });
    }
}

Daftarkan sebelum aplikasi di-build:

builder.Services.AddExceptionHandler<DomainExceptionHandler>();

Melewati IProblemDetailsService itu penting: callback CustomizeProblemDetails yang sama ikut dijalankan, sehingga traceId juga muncul di sini.

Memakainya dari controller

Controller tetap fokus pada alur normal:

[ApiController]
[Route("api/policies")]
public sealed class PoliciesController(IPolicyService policies) : ControllerBase
{
    [HttpGet("{number}")]
    public async Task<ActionResult<PolicyDto>> Get(string number, CancellationToken ct)
    {
        var policy = await policies.FindAsync(number, ct)
            ?? throw new NotFoundException($"Policy '{number}' was not found.");

        return Ok(policy);
    }
}

Untuk hasil yang memang sudah diperkirakan, Anda juga bisa langsung mengembalikan Problem(...) atau NotFound(). Exception lebih praktis ketika aturannya berada jauh di dalam service dan controller tidak perlu tahu soal itu.

Yang tidak boleh masuk ke detail

detail ditampilkan ke client, jadi perlakukan sebagai teks publik:

  • Jangan ada stack trace, SQL, connection string, atau nama host internal.
  • Jangan ada data pribadi yang tidak dikirim sendiri oleh pemanggil.
  • Untuk error 500, biarkan pesan generik bawaan. traceId sudah cukup untuk menemukan error sebenarnya di log.

Menanganinya di client

Karena bentuknya selalu sama, client cukup punya satu pemeta error. Di Angular, misalnya, sebuah HTTP interceptor bisa membaca title, detail, dan dictionary errors dari problem validasi, lalu mengubahnya menjadi toast atau error di form. Tidak ada lagi endpoint yang butuh penanganan khusus.

Ringkasan

  • AddProblemDetails + UseExceptionHandler + UseStatusCodePages sudah mencakup sebagian besar kasus.
  • IExceptionHandler memetakan exception domain ke status code di satu tempat.
  • Tambahkan traceId supaya setiap error yang dilaporkan pengguna bisa dilacak.
  • Pastikan detail aman dibaca siapa saja.