Pengembangan Web

Request Body JSON di PHP - Validasi Media Type, Byte, dan Shape Secara Terpisah

Request Body JSON di PHP - Validasi Media Type, Byte, dan Shape Secara Terpisah

Sebuah endpoint PHP menerima byte, bukan data aplikasi yang otomatis dapat dipercaya. Memanggil json_decode() memang diperlukan ketika byte tersebut memuat JSON, tetapi decode yang berhasil hanya menjawab satu pertanyaan: apakah input cocok dengan sintaks yang diterima decoder? Hasil itu tidak membuktikan bahwa client mengirim media type yang didukung endpoint, ukuran body masih wajar, nilai teratasnya berupa object, atau sebuah field masuk akal untuk operasi tersebut.

Karena itu, desain yang berguna bukan “decode, lalu berharap semuanya baik-baik saja.” Lebih baik gunakan serangkaian keputusan yang lebih sempit. Setiap request yang ditolak seharusnya gagal pada batas yang memang dilanggarnya. Dengan begitu, endpoint lebih mudah dipahami dan client mendapatkan kontrak yang lebih stabil.

Satu request melewati beberapa batas

Ada lima lapisan yang sebaiknya dipisahkan agar tidak melebur menjadi satu cabang “input tidak valid” yang samar:

  1. Media type: apakah client memberi label content dengan format yang didukung endpoint ini?
  2. Byte: apakah server dapat menerima dan membaca content sebanyak ini?
  3. Sintaks: apakah byte tersebut merupakan JSON valid dengan encoding yang diterima decoder?
  4. Shape: apakah nilai hasil decode memiliki object, field, dan tipe PHP yang dibutuhkan endpoint?
  5. Makna: apakah operasi yang diminta diizinkan oleh state bisnis dan otorisasi saat ini?

Lolos dari satu lapisan tidak banyak menjelaskan lapisan berikutnya. Teks JSON true, misalnya, valid secara sintaks. Begitu pula null, string, dan array. RFC 8259 mendefinisikan nilai JSON secara luas; endpoint yang mengharapkan object harus menetapkan aturan yang lebih sempit itu sendiri.

Periksa Content-Type request lebih dahulu

Content-Type menjelaskan bagaimana content dimaksudkan untuk diinterpretasikan. Header ini tidak sama dengan Accept, yang menyatakan preferensi untuk representasi response. Jika kontrak endpoint hanya menerima request body JSON, request non-JSON dapat ditolak dengan 415 Unsupported Media Type. Baik definisi Content-Type dalam RFC 9110 maupun OWASP REST Security Cheat Sheet mendukung pemeriksaan batas ini.

Media type dapat memiliki parameter, sehingga membandingkan seluruh string header dengan application/json akan terlalu rapuh. Nama media type tidak peka huruf besar-kecil. Contoh di bawah mengambil nilai sebelum titik koma pertama, memangkas whitespace, lalu membandingkan bentuk lowercase. Contoh ini sengaja hanya menerima application/json. Keputusan menerima vendor type seperti application/example+json adalah bagian dari kontrak API, bukan pemeriksaan suffix yang harus ditambahkan diam-diam oleh setiap endpoint.

Content-Type yang tidak ada dianggap tidak didukung di sini karena operasi ini memang mewajibkan body JSON. Itu tidak berarti setiap request HTTP tanpa body membutuhkan header tersebut.

Tempatkan batas ukuran paling awal di luar PHP

HTTP mendefinisikan 413 Content Too Large untuk content yang lebih besar daripada yang bersedia atau mampu diproses server. Angka yang tepat bergantung pada operasi: object pengaturan yang pendek dan batch import tidak seharusnya mewarisi batas sama hanya demi kemudahan.

Aplikasi dapat memeriksa strlen($raw), tetapi baru setelah body mencapai PHP dan dibaca menjadi string. Pemeriksaan itu menegakkan kontrak aplikasi; ia bukan batas memory atau denial-of-service yang lengkap. Web server, reverse proxy, atau lapisan penanganan request sebaiknya menerapkan batas lebih awal yang sesuai dengan deployment. Pemeriksaan aplikasi tetap berguna karena membuat asumsi endpoint terlihat jelas dan tetap melindunginya bila konfigurasi upstream berubah.

Content-Length dapat membantu infrastruktur membingkai atau menolak request, tetapi validasi aplikasi tidak boleh menganggap header itu saja sebagai bukti atas byte yang akhirnya diterima. Terapkan batas di tempat content diterima, dan hindari mencatat seluruh body yang ditolak ke log.

Baca raw content, lalu decode secara eksplisit

Untuk endpoint native PHP, php://input menyediakan stream read-only untuk data mentah request body. Superglobal $_POST bukan interface untuk raw JSON body. Membaca stream tersebut membuat perpindahan dari content HTTP menjadi string aplikasi terlihat jelas.

Decoder juga membutuhkan kebijakan eksplisit. Dokumentasi PHP menjelaskan bahwa json_decode() menerima input UTF-8, memiliki batas maksimum nesting, dan mendukung JSON_THROW_ON_ERROR. RFC 8259 juga mewajibkan JSON yang dipertukarkan melalui jaringan memakai UTF-8. Dengan flag exception, JSON null yang valid tidak lagi tertukar dengan nilai error default berupa null. PHP melempar JsonException untuk kegagalan decode saat flag tersebut aktif.

Contoh ini menangkap exception tersebut dan mengembalikan satu pesan yang terbatas kepada client. Secara internal, sintaks rusak, UTF-8 tidak valid, dan nesting berlebihan dapat dicatat sebagai kategori diagnostik berbeda jika berguna, tetapi detail parser jarang perlu dibuka oleh API publik kecil.

Contoh PHP yang ringkas

<?php

declare(strict_types=1);

function respondWithError(int $status, string $detail): never
{
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode(['error' => $detail], JSON_THROW_ON_ERROR);
    exit;
}

$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$mediaType = strtolower(trim(explode(';', $contentType, 2)[0]));

if ($mediaType !== 'application/json') {
    respondWithError(415, 'This endpoint accepts application/json.');
}

$raw = file_get_contents('php://input');
if ($raw === false) {
    throw new RuntimeException('Could not read the request body.');
}

$maxBytes = 64 * 1024; // Example policy, not a universal limit.
if (strlen($raw) > $maxBytes) {
    respondWithError(413, 'The request body is too large.');
}

if ($raw === '') {
    respondWithError(400, 'A JSON request body is required.');
}

try {
    $data = json_decode(
        $raw,
        associative: false,
        depth: 64,
        flags: JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING
    );
} catch (JsonException) {
    respondWithError(400, 'The request body is not valid JSON.');
}

if (!$data instanceof stdClass) {
    respondWithError(422, 'The top-level JSON value must be an object.');
}

$fields = get_object_vars($data);
if (!array_key_exists('title', $fields) || !is_string($fields['title'])) {
    respondWithError(422, 'The title field must be a string.');
}

$title = trim($fields['title']);
if ($title === '' || mb_strlen($title) > 160) {
    respondWithError(422, 'The title must contain 1 to 160 characters.');
}

// Authentication, authorization, persistence, and conflict checks belong here.
http_response_code(204);

Batas body 64 KiB dan depth nesting 64 adalah kebijakan contoh, bukan nilai aman yang universal. Keduanya perlu dipilih berdasarkan dokumen yang diharapkan endpoint dan batas deployment. Helper response ini juga sengaja minimal; API yang sudah memakai Problem Details dapat mempertahankan urutan pemrosesan ini sambil mengembalikan format error yang sudah ditetapkan.

Sintaks dan shape membutuhkan keputusan berbeda

Decode menjadi object mempertahankan perbedaan penting pada level teratas. Jika operasi mewajibkan JSON object, array atau scalar tidak seharusnya masuk ke validasi field melalui coercion PHP. Setelah pemeriksaan object, get_object_vars() membuka field-nya tanpa berpura-pura bahwa data itu sudah menjadi domain object yang tepercaya.

Contoh ini membedakan JSON rusak dengan 400 Bad Request dari JSON valid yang tidak cocok dengan shape aplikasi menggunakan 422 Unprocessable Content. RFC 9110 menjelaskan 422 sebagai content dengan sintaks benar tetapi instruksi di dalamnya tidak dapat diproses. Ini adalah pemetaan yang koheren, bukan satu-satunya konvensi API. Bagian pentingnya adalah mendokumentasikan satu kebijakan dan menerapkannya secara konsisten.

Validasi field juga sebaiknya membedakan “tidak ada” dari “ada dengan tipe yang salah.” Pemakaian array_key_exists() membuat keputusan itu eksplisit. String kemudian di-trim dan diperiksa berdasarkan aturan panjang operasi. Escaping sengaja tidak dilakukan: output encoding seharusnya diterapkan pada konteks output akhirnya, sedangkan nilai database seharusnya masuk melalui parameterized query. Mengubah input agar sekadar tampak lebih aman dapat menyembunyikan pelanggaran kontrak.

Detail yang tidak dapat diselesaikan contoh kecil

Nama object yang duplikat

RFC 8259 menyatakan nama member object sebaiknya unik dan memperingatkan bahwa perilaku penerima berbeda ketika nama tidak unik. json_decode() biasa bukan validator nama duplikat. Jika deteksi duplikat penting untuk signature, audit, atau protokol ketat, gunakan pendekatan parsing yang dapat mendeteksinya sebelum member melebur menjadi object PHP.

Angka dan identifier

Contoh memakai JSON_BIGINT_AS_STRING agar token integer yang cukup besar tidak diubah menjadi nilai floating-point yang tidak presisi. Flag itu bukan kebijakan numerik yang lengkap. Uang, pengukuran, range, dan aritmetika tetap memerlukan penanganan khusus domain. Nilai seperti account ID sering lebih baik didefinisikan sebagai string JSON ketika nilainya merupakan identifier, bukan kuantitas.

Field yang tidak dikenal

Keputusan menolak member yang tidak dikenal merupakan pilihan kompatibilitas. Penolakan membantu menemukan salah ketik lebih awal; mengabaikannya dapat memudahkan evolusi aditif. Kedua kebijakan bisa masuk akal, tetapi inkonsistensi tidak sengaja antar-endpoint lebih menyulitkan client dibanding salah satu pilihan yang terdokumentasi.

Otorisasi dan state

Object dengan shape sempurna masih dapat meminta operasi yang tidak boleh dilakukan pemanggil, merujuk resource yang sudah tidak ada, atau bertentangan dengan state terkini. Parsing dan validasi tidak menggantikan authentication, authorization, concurrency control, atau database constraint. Keduanya hanya menghasilkan input yang lebih aman untuk keputusan berikutnya.

Checklist berlapis

  • Tentukan media type request yang diterima endpoint.
  • Terapkan batas ukuran body lebih awal pada lapisan web atau proxy serta kebijakan yang sesuai di aplikasi.
  • Baca raw body dari php://input.
  • Decode dengan depth eksplisit dan penanganan error yang tidak ambigu.
  • Wajibkan tipe JSON level teratas yang dimaksud.
  • Validasi keberadaan field, tipe PHP, range, panjang, dan format tanpa coercion diam-diam.
  • Tentukan cara menangani field asing, nama duplikat, dan angka besar.
  • Baru setelah itu evaluasi otorisasi, state aplikasi, dan persistence.

Tujuannya bukan mengubah endpoint kecil menjadi upacara panjang. Tujuannya adalah menghindari meminta satu fungsi menjawab lima pertanyaan berbeda. Ketika media type, byte, sintaks, shape, dan makna memiliki batas masing-masing, kegagalan lebih mudah dijelaskan dan perubahan lebih mudah ditinjau.

References