Pengembangan Web

Problem Details untuk API PHP Kecil - Beri Error Bentuk Stabil Tanpa Menyembunyikan HTTP

Problem Details untuk API PHP Kecil - Beri Error Bentuk Stabil Tanpa Menyembunyikan HTTP

Sebuah API bisa mengembalikan 422, tetapi client mungkin masih perlu tahu apakah judul belum diisi, tanggal tidak valid, atau transisi state tidak lagi diizinkan. Status code memberi makna HTTP secara umum pada response. Namun, status code saja tidak menyediakan kontrak error level aplikasi yang stabil.

Reaksi yang umum adalah membuat envelope JSON sendiri: mungkin {"error":"invalid"} pada satu endpoint dan {"success":false,"message":"..."} pada endpoint lain. Cara ini berjalan sampai client harus mempelajari beberapa bentuk, prosa untuk manusia tanpa sengaja menjadi identifier, atau intermediary menerima 200 OK untuk request yang sebenarnya gagal.

RFC 9457, Problem Details for HTTP APIs, menawarkan solusi yang lebih sempit. Standar ini menetapkan object kecil untuk menjelaskan error HTTP API tanpa menggantikan semantik status HTTP atau menentukan business rule sebuah aplikasi. Artikel ini membahas cara API PHP kecil dapat menggunakan format tersebut secara jujur: arti setiap field, bagian yang tetap spesifik untuk aplikasi, dan informasi yang harus tetap berada di luar response.

Status code dan body memiliki tugas berbeda

HTTP sudah memiliki kosakata untuk hasil umum sebuah request. RFC 9110 bagian 15 menetapkan semantik status code yang dapat dipahami client, proxy, cache, dan software generik lain. 404 menyatakan bahwa origin server tidak menemukan representasi terkini dari resource target; 409 menjelaskan konflik dengan state resource saat ini; 500 menjelaskan kondisi server yang tidak terduga.

Kategori tersebut harus tetap dikirim sebagai status HTTP yang sebenarnya. Problem Details menambahkan body terstruktur untuk makna yang spesifik bagi API. Format ini tidak membenarkan pengembalian 200 sambil menyembunyikan kegagalan di dalam JSON. Format ini juga tidak mewajibkan body ketika status code sudah menyampaikan semua informasi yang berguna.

RFC 9457 menyatakan bahwa format ini dapat digunakan bersama status code apa pun, meskipun paling alami untuk response 4xx dan 5xx. RFC tersebut juga mengingatkan bahwa format domain yang sudah ada mungkin lebih tepat. Karena itu, adopsinya merupakan keputusan kontrak, bukan klaim bahwa setiap error response di Web harus memiliki bentuk yang sama.

Lima member standar bukan lima kotak wajib

Dokumen problem dalam JSON menggunakan media type application/problem+json. Standar tersebut menetapkan lima member, semuanya opsional dalam JSON schema, dengan peran yang berbeda:

MemberPeranAsumsi yang sebaiknya dibuat client
typeURI reference yang mengidentifikasi problem typeSetelah resolusi URI, inilah identitas utama yang machine-readable.
statusStatus code HTTP dari origin server untuk kejadian iniNilainya bersifat advisory; perilaku HTTP generik mengikuti status line pada response.
titleRingkasan singkat problem type yang dapat dibaca manusiaNilainya sebaiknya stabil antar-kejadian, kecuali untuk lokalisasi.
detailPenjelasan yang dapat dibaca manusia untuk kejadian iniTampilkan bila sesuai, tetapi jangan parse sebagai identifier.
instanceURI reference yang mengidentifikasi kejadian khusus iniNilainya dapat berupa correlation identifier yang opaque dan tidak harus bisa diambil.

Jika type tidak ada, nilainya diperlakukan sebagai about:blank. Type ini berarti problem tersebut tidak memiliki semantik tambahan di luar status code HTTP. Dalam keadaan ini, title sebaiknya sama dengan status phrase yang direkomendasikan, dengan kemungkinan lokalisasi. Response not-found sederhana dapat memakai default ini tanpa membuat URI rumit yang hanya mengganti nama 404.

Member status sengaja menduplikasi status sebenarnya, misalnya ketika body disimpan tanpa metadata HTTP-nya. Generator yang menyertakannya wajib mengirim status yang sama pada response. Jika proxy kemudian mengubah status response, keduanya dapat berbeda; ini salah satu alasan client tidak dapat memperlakukan angka di JSON sebagai pengganti HTTP.

Problem type adalah kontrak stabil, bukan sebuah kalimat

Misalnya, seorang editor mencoba menerbitkan artikel sebelum field wajibnya valid. API dapat mengidentifikasi kondisi tersebut dengan:

{
  "type": "https://example.com/problems/article-not-publishable",
  "title": "The article is not ready to publish.",
  "status": 422,
  "detail": "Correct the highlighted fields and submit again.",
  "instance": "https://example.com/problem-occurrences/7f3a2c"
}

Client sebaiknya membuat percabangan berdasarkan URI type yang sudah di-resolve, bukan berdasarkan title atau detail dalam bahasa Inggris. Susunan kata untuk manusia dapat diperbaiki, dan lokalisasi dapat mengubahnya sepenuhnya, tanpa menciptakan problem type baru. Sebaliknya, mengubah makna type merupakan perubahan kontrak meskipun URI-nya tetap sama.

RFC 9457 mendorong penggunaan type URI yang dapat di-resolve. Ketika URI type HTTP atau HTTPS dibuka oleh developer, URI tersebut sebaiknya menyediakan dokumentasi yang dapat dibaca manusia: makna, status code yang biasa digunakan, extension yang relevan, dan kemungkinan pemulihan. Client normal sebaiknya tidak otomatis mengambil dokumentasi tersebut setiap kali menangani error. URI itu pertama-tama adalah identifier, bukan instruksi untuk menambahkan dependency jaringan baru.

Sebelum menetapkan type, perancang API juga dapat memeriksa registry HTTP Problem Types IANA yang terus berkembang. Kebijakan registrasinya ditujukan bagi type umum yang didukung spesifikasi, bukan setiap kondisi validasi privat milik aplikasi. Type khusus aplikasi dapat ditempatkan di ruang URI stabil yang dikendalikan aplikasi tersebut.

Responder PHP yang sengaja dibuat kecil

PHP sudah menyediakan tiga operasi dasar yang dibutuhkan: menetapkan status response, menetapkan content type sebelum output, dan meng-encode JSON. Helper berikut melindungi nama member standar agar tidak diganti extension dan meminta json_encode() melempar exception jika proses encoding gagal:

<?php
declare(strict_types=1);

function sendProblem(
    int $status,
    string $type,
    string $title,
    string $detail,
    ?string $instance = null,
    array $extensions = []
): void {
    $problem = [
        'type' => $type,
        'title' => $title,
        'status' => $status,
        'detail' => $detail,
    ];

    if ($instance !== null) {
        $problem['instance'] = $instance;
    }

    foreach ($extensions as $name => $value) {
        if (array_key_exists($name, $problem) || $name === 'instance') {
            throw new InvalidArgumentException('Extension name is reserved.');
        }
        $problem[$name] = $value;
    }

    $json = json_encode(
        $problem,
        JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
    );

    http_response_code($status);
    header('Content-Type: application/problem+json');
    echo $json;
    exit;
}

Manual PHP mendokumentasikan http_response_code() sebagai pengatur status, sedangkan header() harus dijalankan sebelum output dikirim. Dokumentasi json_encode() juga menyatakan bahwa string input harus menggunakan UTF-8. Melakukan encoding sebelum mengirim header mencegah helper ini secara sengaja mengirim setengah dokumen problem jika encoding JSON melempar exception.

Ini bukan exception handler yang lengkap. Aplikasi nyata tetap memerlukan pemetaan terpusat dari domain failure yang dikenal ke problem type publik, jalur terpisah untuk exception tak terduga, logging, lokalisasi, otorisasi, dan pengujian melalui web server yang sebenarnya. Helper ini juga mengasumsikan pemanggil menyediakan status 4xx atau 5xx yang masuk akal dan string yang aman. Menjaganya tetap kecil membuat tanggung jawab yang belum tercakup terlihat jelas, alih-alih berpura-pura bahwa satu fungsi telah menyelesaikan error handling.

Detail validasi sebaiknya berada dalam extension terdokumentasi

Sebuah form dapat gagal pada beberapa field tetapi tetap mewakili satu problem type: representasi yang dikirim tidak valid. RFC 9457 menggambarkannya dengan extension errors. Bentuk extension tersebut merupakan bagian dari definisi problem type; errors bukan member standar universal yang otomatis dipahami setiap client.

sendProblem(
    422,
    'https://example.com/problems/invalid-article',
    'The article is not valid.',
    'Correct the listed fields and submit again.',
    '/problem-occurrences/7f3a2c',
    [
        'errors' => [
            ['pointer' => '#/title', 'code' => 'required'],
            ['pointer' => '#/publish_at', 'code' => 'invalid_date'],
        ],
    ]
);

Di sini, pointer menunjukkan lokasi input dan code menyediakan token aplikasi yang stabil. Penjelasan untuk manusia dapat ditambahkan jika kontraknya membutuhkan. Client wajib mengabaikan extension member yang tidak dikenali, sehingga sebuah problem type dapat memperoleh metadata kompatibel di kemudian hari. Server juga sebaiknya tidak diam-diam menjadikan extension opsional baru sebagai kewajiban bagi client lama.

Beberapa kegagalan yang tidak saling berkaitan lebih sulit ditangani. RFC 9457 merekomendasikan agar problem yang paling relevan atau mendesak direpresentasikan ketika kegagalan tersebut tidak memiliki type yang sama. Array generik berisi problem yang berbeda mungkin dapat dibuat, tetapi bentuk itu sulit dipetakan ke semantik satu response HTTP. Operasi batch memerlukan desain domain yang eksplisit, bukan asumsi bahwa Problem Details menetapkan satu batch envelope universal.

Detail yang berguna dapat berubah menjadi kebocoran informasi

Format error membuat informasi konsisten; format tersebut tidak menentukan informasi mana yang aman. Bagian keamanan RFC 9457 memperingatkan tentang pengungkapan detail implementasi, akses sistem, dan privasi pengguna. OWASP REST Security Cheat Sheet juga merekomendasikan pesan publik generik, bukan call stack atau petunjuk internal.

detail publik sebaiknya membantu pemanggil memperbaiki request, bukan mengungkap SQL statement, path filesystem, access token, record privat, atau stack trace. Kegagalan tak terduga dapat mengembalikan type generik yang stabil dan identifier instance yang opaque, sementara log terlindungi menyimpan konteks diagnostik. Identifier tersebut tidak boleh mengodekan secret, dan endpoint apa pun yang dapat membukanya memerlukan kebijakan otorisasi sendiri.

Perbedaan error juga dapat membocorkan state. Mengembalikan “akun ada tetapi password salah” kepada client login anonim mengungkap lebih banyak daripada “credential tidak diterima.” Problem Details tidak mengesampingkan threat modeling. Terkadang dua penyebab internal memang sebaiknya menggunakan type, title, dan detail publik yang sama.

Uji kontrak di wire, bukan hanya array PHP

Test suite yang berguna sebaiknya mengamati response sebagaimana diterima client:

  • Status HTTP yang sebenarnya dan status JSON opsional memiliki nilai yang sama.
  • Response menggunakan application/problem+json dan memuat JSON yang valid.
  • Type yang dikenal mempertahankan semantik, kebijakan title, dan extension terdokumentasinya.
  • Prosa yang dilokalkan tidak mengubah machine identifier.
  • Client menoleransi extension member yang tidak dikenal.
  • Exception tak terduga tidak membocorkan stack, query, path, credential, atau object privat.
  • Nilai instance mendukung korelasi tanpa memberikan akses yang tidak dimaksudkan.
  • Proxy dan middleware framework tidak mengganti status atau content type yang diinginkan.

Migrasi juga membutuhkan kehati-hatian yang sama. Jika API yang sudah ada telah memiliki format error stabil, biaya menggantinya mungkin lebih besar daripada manfaatnya. Content negotiation, endpoint berversi, atau transisi terdokumentasi dapat lebih aman daripada mengubah semua error body secara langsung. RFC 9457 dibuat untuk menghindari format baru yang tidak perlu, bukan untuk menghapus kontrak yang berfungsi tanpa mempertimbangkan konsumennya.

Kesimpulan

Problem Details memberi API PHP kecil pembagian tugas yang berguna. Status code HTTP mempertahankan makna protokolnya. type yang stabil mengidentifikasi problem aplikasi. title dan detail berbicara kepada manusia, instance dapat mengorelasikan satu kejadian, dan extension terdokumentasi membawa informasi terstruktur yang menjadi bagian dari type tersebut.

Format ini sengaja tidak lengkap. Ia tidak memilih status yang tepat, merancang validation code, mengamankan data diagnostik, atau membuat API yang tidak stabil menjadi stabil. Semua itu tetap merupakan tanggung jawab desain. Jika digunakan dengan memahami batas tersebut, RFC 9457 dapat membuat error lebih mudah diprediksi tanpa berpura-pura bahwa satu bentuk JSON mampu menjelaskan setiap kegagalan.

References