Fitur Review Dimatikan, Kok Balasnya 404 Bukan 403?
Mematikan fitur rating di portal kota ternyata dibalas 404. Ternyata spesifikasi HTTP memang mendukung pilihan menyembunyikan keberadaan itu.
Ringkasan
Awalnya kukira endpoint rating error karena balik 404 bukan 403 pas fiturnya dimatiin. Ternyata emang disengaja lewat gate ensureRatingsAllowed yang ngecek rating_enabled kalau false langsung lempar ErrRatingsUnavailable jadi 404 biar kesannya fitur nggak ada. Cara ini lebih aman soalnya nggak bocorin info, sekaligus nge-cover tiga endpoint sekaligus dengan query enteng.
Saya lagi ngecek log request di sebuah project portal kota. Fitur rating baru saja dimatikan dari sisi database karena alasan bisnis sementara. Saya buka endpoint distribusi rating untuk memastikan penutupan ini berjalan mulus dan nggak ada error yang nggak terduga. Yang muncul di log malah baris error singkat yang bikin saya mengerutkan dahi: 404 Not Found.
Ekspektasi 403 yang Meleset
Jujur, reaksi pertama saya agak bingung. Logika dasar saya bilang, kalau sebuah fitur sengaja dimatikan atau user nggak punya akses, server seharusnya balas 403 Forbidden. Bukankah server paham permintaannya, cuma menolak memprosesnya karena aturan bisnis? Saya sempat mikir ini bug di router atau konfigurasi middleware yang kelewat. Mungkin ada rule di reverse proxy yang salah tangkap, atau middleware autentikasi yang gagal memvalidasi token dengan benar.
Saya coba telusuri kode, cari kondisi if !featureEnabled { return 403 }. Ternyata bukan bug, dan bukan juga 403. Ini keputusan desain yang disengaja.
Logika di Balik Layar
Saat menelusuri kode lebih dalam, saya menemukan gate ensureRatingsAllowed yang ditanam rapi di service layer. Fungsi ini melakukan probe boolean sederhana ke database. Query-nya sangat spesifik dan efisien, cuma mengecek kolom rating_enabled pada entitas yang berstatus published dan deleted_at IS NULL.
Kalau row-nya hilang atau nilainya false, sistem tidak langsung panik atau mencoba mengembalikan data kosong. Sistem langsung melempar error domain kecil bernama ErrRatingsUnavailable. Di bagian handler HTTP, error spesifik ini sengaja dipetakan menjadi status 404, bukan 403.
Awalnya saya ragu, tapi spesifikasi HTTP justru mendukung pendekatan ini sepenuhnya. RFC 9110 dengan jelas menyatakan bahwa server boleh mengembalikan 404 jika tidak menemukan representasi terkini, atau jika server tidak mau mengakui keberadaan resource tersebut [7]. Dokumentasi MDN juga memperkuat hal ini, menyebutkan bahwa server diperbolehkan mengirim 404 alih-alih 403 ketika tidak ingin membocorkan keberadaan resource kepada klien yang tidak berhak [6].
Risiko Membalas 200 OK dengan Data Kosong
Beberapa orang mungkin berpikir, kenapa nggak balas 200 OK aja dengan array kosong atau objek null? Secara teknis itu mungkin dilakukan, tapi ini membuka celah side-channel yang nggak perlu. Kalau endpoint balas 200 OK, klien tahu endpoint itu valid dan aktif. Mereka bisa mulai mencoba variasi parameter, mencari celah pagination, atau melihat perbedaan waktu respons antara request yang valid dan yang tidak.
Dengan mengembalikan 404, kita memutus rantai asumsi itu sejak awal. Server bertindak seolah-olah rute yang diminta memang tidak pernah ada di sistem ini. Ini adalah bentuk pertahanan pasif yang sangat efektif untuk menjaga integritas arsitektur.
Satu Gate untuk Tiga Endpoint
Dengan pola ini, kita dapat keuntungan keamanan yang nyata. Kalau kita pakai 403, kita secara tidak langsung memberi tahu klien bahwa fitur ini ada di sini, cuma sedang dimatikan. Itu adalah informasi yang nggak perlu kita sebarkan ke publik. Dengan 404, fitur itu seolah-olah tidak pernah ada di mata klien, mencegah teknik enumerasi endpoint di mana penyerang mencoba menebak-nebak fitur tersembunyi.
Implementasinya juga ternyata lebih rapi. Saya cukup memasang satu gate di service layer yang melindungi tiga endpoint sekaligus: pembuatan review baru, daftar review yang disetujui, dan distribusi rating. Probe boolean-nya cuma berjalan sekali per request:
SELECT rating_enabled WHERE status = 'published' AND deleted_at IS NULL
Kita nggak perlu melakukan JOIN yang kompleks. Jika query mengembalikan baris, kita cek nilainya. Jika tidak ada baris sama sekali, kita anggap false. Overhead pengecekan jadi hampir nol.
Di bagian handler, pemetaan error ini dilakukan dengan sangat eksplisit. Ketika ErrRatingsUnavailable tertangkap, handler langsung mengembalikan respons 404 dengan body yang minimalis. Di sisi DTO publik, flag rating_enabled dan rental_enabled ikut diekspos agar klien bisa menyesuaikan antarmuka di awal, sementara type_id dari entitas tetap tidak dibocorkan supaya detail skema tetap rapat.
Mengubah pola pikir dari "menolak akses" menjadi "menyembunyikan keberadaan" memang butuh penyesuaian. Tapi setelah melihat betapa bersihnya log dan seberapa sedikit informasi yang bocor ke publik, saya yakin ini langkah yang tepat. Kadang, jawaban terbaik untuk pertanyaan yang tidak seharusnya diajukan adalah pura-pura kita tidak mendengarnya sama sekali.