Jika sebuah endpoint API kadang berhasil, kadang 404 atau 405 untuk request yang tampaknya identik, salah satu tersangka paling sering adalah normalisasi path. Masalah ini muncul saat komponen di sepanjang jalur request—load balancer, reverse proxy, framework, router, atau service upstream—tidak sepakat tentang apakah //, %2F, atau trailing slash harus dipertahankan, dibersihkan, atau di-decode.

Efeknya terasa acak: request dari satu klien berhasil, dari klien lain gagal; di lingkungan staging normal, di production pecah; melalui proxy A lolos, lewat proxy B menjadi 404 atau bahkan 405. Artikel ini membahas studi kasus praktis untuk debug API rapuh: normalisasi path yang memicu 404 acak, dari gejala di log sampai langkah perbaikan yang aman.

Kenapa request yang tampak setara bisa diperlakukan berbeda?

Secara visual, path berikut sering dianggap setara oleh manusia:

  • /v1/orders/123
  • /v1/orders//123
  • /v1/orders/123/
  • /v1/orders/%31%32%33 untuk bagian ID yang di-encode
  • /v1/files/a%2Fb versus /v1/files/a/b

Namun bagi sistem HTTP, itu belum tentu sama. Ada beberapa sumber perbedaan:

  • Slash ganda: sebagian proxy merapikan // menjadi /, sebagian meneruskannya apa adanya.
  • Trailing slash: router dapat menganggap /users dan /users/ sebagai route berbeda.
  • URL decoding: beberapa lapisan melakukan decode lebih awal, sehingga %2F berubah menjadi slash sungguhan dan mengubah struktur path.
  • Method mapping: path yang ternormalisasi ke route berbeda bisa menghasilkan 404 Not Found atau 405 Method Not Allowed, tergantung apakah route ada tetapi method-nya tidak cocok.

Intinya, bug ini jarang berada di satu titik saja. Biasanya ia lahir dari kombinasi perilaku yang tampak masuk akal jika dilihat terpisah, tetapi berbahaya saat digabungkan.

Studi kasus: endpoint upload yang kadang 404, kadang 405

Bayangkan arsitektur sederhana berikut:

  • Klien mengirim request ke reverse proxy
  • Proxy meneruskan ke aplikasi backend
  • Backend memanggil service upstream internal untuk penyimpanan file

Tim menemukan gejala berikut pada endpoint:

POST /api/files/upload

Gejalanya:

  • Dari aplikasi web: sering berhasil
  • Dari job internal atau script tertentu: kadang 404
  • Dari integrasi partner: kadang 405

Di log aplikasi, request terlihat mirip:

POST /api/files/upload        - 200
POST /api//files/upload       - 404
POST /api/files/upload/       - 405

Pada pandangan pertama, ini seperti bug routing biasa. Tetapi setelah dicek lebih dalam, akar masalahnya adalah:

  1. Beberapa klien membentuk URL dengan cara menggabungkan base URL dan path tanpa sanitasi, sehingga menghasilkan //.
  2. Reverse proxy di satu environment membiarkan slash ganda lewat, sementara environment lain merapikannya.
  3. Framework backend melakukan redirect atau pencocokan route berbeda untuk trailing slash.
  4. Service upstream menganggap %2F sebagai bagian data, tetapi lapisan sebelumnya sudah telanjur mengubahnya menjadi pemisah path.

Gejala yang terlihat di log dan kenapa sering menyesatkan

1. Log access tidak selalu menunjukkan path asli

Banyak stack hanya mencatat path setelah diproses. Akibatnya Anda bisa melihat /api/files/upload di log aplikasi, padahal klien sebenarnya mengirim /api//files/upload atau path yang masih ter-encode.

Jika ada middleware, proxy, atau framework yang menormalkan URL lebih awal, jejak request mentah bisa hilang.

2. 404 dan 405 bisa muncul untuk akar masalah yang sama

Misalnya:

  • 404 muncul ketika path hasil normalisasi tidak cocok ke route mana pun.
  • 405 muncul ketika path hasil normalisasi cocok ke route lain, tetapi method tidak diizinkan.

Ini membuat tim sering salah fokus ke daftar method atau auth, padahal masalah aslinya tetap path.

3. Bug tampak acak karena bergantung pada jalur request

Request dari browser, mobile app, worker internal, dan partner sering melewati jalur berbeda:

  • proxy berbeda
  • library HTTP berbeda
  • builder URL berbeda
  • CDN atau gateway berbeda

Itulah sebabnya bug ini terasa intermittent, padahal sebenarnya deterministik untuk input dan jalur tertentu.

Root cause teknis yang paling sering

Perbedaan trailing slash

Sebagian router memperlakukan /users dan /users/ sebagai dua path berbeda. Sebagian lagi akan redirect secara implisit. Masalah muncul jika proxy atau klien mengubah salah satunya, sementara route hanya didaftarkan dalam satu bentuk.

Slash ganda saat join URL

Bug klasik:

const baseUrl = 'https://api.example.com/';
const path = '/v1/orders/123';
const url = baseUrl + path;
// hasil: https://api.example.com//v1/orders/123

Beberapa server akan menerima ini, beberapa akan menormalkan, beberapa tidak. Jika ada aturan rewrite berbasis prefix, slash ganda dapat mengubah hasil pencocokan.

Decoding terlalu dini

Path seperti /files/a%2Fb berbahaya jika aplikasi bermaksud memperlakukan a/b sebagai satu identifier logis, bukan dua segmen path. Jika proxy atau framework melakukan decode lebih awal, route yang semula valid bisa pecah menjadi struktur path berbeda.

Masalah ini juga sensitif dari sisi keamanan: canonicalization yang tidak konsisten dapat memicu bypass rule, cache key mismatch, atau akses ke resource yang salah.

Mismatch antara path yang diroute dan path yang diteruskan upstream

Ada kasus di mana aplikasi backend mencocokkan route berdasarkan satu representasi path, tetapi meneruskan request ke upstream menggunakan representasi lain. Akibatnya endpoint internal yang tampak sama ternyata dipukul dengan path yang sudah berubah.

Langkah isolasi bug yang efektif

1. Catat path mentah sedekat mungkin dengan edge

Tujuannya adalah melihat apa yang benar-benar dikirim klien sebelum dinormalisasi. Praktiknya:

  • Tambahkan logging di reverse proxy untuk request target mentah jika memungkinkan.
  • Catat header penting seperti X-Forwarded-* dan request ID.
  • Di aplikasi, log beberapa bentuk path bila tersedia: request URI mentah, path yang sudah di-decode, dan route yang akhirnya cocok.

Jangan hanya mengandalkan satu log aplikasi.

2. Korelasikan log lintas lapisan

Pasang request ID yang sama dari proxy hingga upstream. Tanpa korelasi ini, Anda akan kesulitan membandingkan:

  • path yang diterima proxy
  • path yang dilihat framework
  • path yang diteruskan ke upstream
  • response status di tiap lapisan

3. Uji variasi path secara eksplisit

Jangan menguji hanya endpoint normal. Buat matriks kecil untuk path yang tampak setara:

  • /api/files/upload
  • /api/files/upload/
  • /api//files/upload
  • /api/files//upload
  • /api/files/%75pload bila relevan
  • /api/files/a%2Fb untuk path parameter yang sensitif

Tujuannya bukan mencari semua kombinasi, tetapi menemukan titik di mana perilaku mulai bercabang.

Reproduksi dengan curl dan integration test

curl untuk membedakan perilaku antar lapisan

Gunakan curl dengan path yang disengaja. Contoh:

curl -i -X POST 'https://api.example.com/api/files/upload'

curl -i -X POST 'https://api.example.com/api//files/upload'

curl -i -X POST 'https://api.example.com/api/files/upload/'

curl -i -X GET 'https://api.example.com/api/files/upload'

curl -i 'https://api.example.com/api/files/a%2Fb'

Perhatikan:

  • status code
  • header Location jika ada redirect
  • apakah response datang dari proxy, app, atau upstream

Jika memungkinkan, lakukan request yang sama ke:

  • domain public di depan proxy
  • service backend langsung
  • upstream internal langsung

Selisih perilaku di tiga titik ini sering langsung mempersempit area bug.

Integration test untuk mencegah regresi

Setelah pola bug ditemukan, ubah menjadi test. Contoh pseudo-test:

cases = [
  { method: 'POST', path: '/api/files/upload', expected: 200 },
  { method: 'POST', path: '/api/files/upload/', expected: 308 },
  { method: 'POST', path: '/api//files/upload', expected: 400 },
  { method: 'GET',  path: '/api/files/upload', expected: 405 }
]

for case in cases:
  response = http_request(case.method, case.path)
  assert response.status == case.expected

Angka status di atas hanya contoh pola. Pilihan final bergantung pada kontrak API Anda. Yang penting adalah perilaku terhadap path non-kanonis harus eksplisit dan konsisten.

Perbaikan yang aman: pilih aturan canonicalization yang jelas

1. Tentukan bentuk path kanonis

Sebelum mengubah konfigurasi, putuskan dulu kontraknya:

  • Apakah API menerima trailing slash atau tidak?
  • Apakah slash ganda dianggap invalid atau dirapikan?
  • Apakah encoded slash boleh muncul di path parameter?

Keputusan ini harus terdokumentasi. Tanpa kontrak yang jelas, tiap lapisan akan membuat asumsi sendiri.

2. Tolak input ambigu lebih awal

Untuk API, sering kali lebih aman menolak path ambigu daripada diam-diam menormalkan semuanya. Contohnya:

  • // di path dikembalikan sebagai 400 Bad Request
  • trailing slash pada endpoint yang tidak mendukung dikembalikan sebagai redirect atau 404 secara konsisten
  • encoded slash di path ditolak jika sistem tidak didesain untuk menanganinya

Kenapa? Karena normalisasi otomatis dapat menyamarkan bug klien dan mempersulit debugging di masa depan.

3. Samakan perilaku proxy dan aplikasi

Jangan biarkan proxy dan backend punya aturan berbeda. Prinsip praktisnya:

  • jika proxy merapikan path, aplikasi harus tahu implikasinya
  • jika aplikasi sensitif terhadap path mentah, proxy jangan mengubahnya diam-diam
  • rewrite rule harus diuji terhadap slash ganda, trailing slash, dan URL-encoded segment

Di banyak insiden, bug bukan karena proxy atau framework salah, melainkan karena keduanya benar menurut aturan masing-masing tetapi tidak selaras.

4. Hindari join URL dengan konkatenasi mentah

Di sisi klien atau service internal, gunakan helper yang memastikan penggabungan base URL dan path konsisten. Jika tidak ada utilitas tepercaya, setidaknya normalisasikan satu sisi:

function joinUrl(base, path) {
  return base.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
}

Ini tidak menyelesaikan semua kasus URL, tetapi cukup untuk menghindari slash ganda akibat konkatenasi naif. Tetap uji hasilnya jika ada query string, path parameter ter-encode, atau base path khusus.

5. Waspadai decode path parameter

Jika identifier dapat mengandung karakter yang biasanya spesial dalam URL, pertimbangkan desain yang lebih aman:

  • gunakan query parameter alih-alih path segment
  • gunakan encoding yang tidak ambigu untuk identifier
  • hindari membiarkan slash menjadi bagian dari identifier path kecuali benar-benar diperlukan

Semakin kompleks aturan decoding, semakin tinggi peluang perbedaan interpretasi antar komponen.

Contoh guardrail yang layak dipasang

Canonicalization policy

Buat aturan tertulis seperti:

  • semua endpoint API menggunakan path tanpa trailing slash
  • double slash ditolak dengan 400
  • encoded slash di path tidak didukung
  • redirect hanya digunakan untuk endpoint browser-facing, bukan API machine-to-machine, kecuali memang disepakati

Dengan aturan seperti ini, perilaku sistem menjadi mudah diprediksi.

Kontrak path yang ketat di dokumentasi API

Dokumentasi sering menjelaskan body dan auth, tetapi mengabaikan semantik path. Padahal di kasus ini, itu bagian penting dari kontrak. Tulis dengan eksplisit:

  • bentuk path yang valid
  • apakah trailing slash diterima
  • perlakuan terhadap karakter ter-encode
  • status code untuk path non-kanonis

Observability khusus routing

Tambahkan sinyal yang membantu saat ada insiden:

  • counter untuk 404/405 per route prefix
  • log path mentah versus path ternormalisasi
  • atribut tracing untuk route template dan matched handler
  • alert ketika pola // atau %2F melonjak

Dengan ini, Anda tidak perlu menunggu keluhan pengguna untuk tahu ada perubahan perilaku.

Regression test di level proxy dan aplikasi

Jangan hanya menulis unit test router. Tambahkan test yang benar-benar memukul stack HTTP Anda, minimal di CI atau environment test:

  • request ke endpoint kanonis
  • request dengan trailing slash
  • request dengan slash ganda
  • request dengan path ter-encode yang relevan

Jika sistem melibatkan reverse proxy, idealnya ada satu lapis test yang melewati proxy tersebut, karena banyak bug justru muncul di sana.

Kesalahan umum saat menangani bug ini

  • Menyalahkan auth atau cache terlalu cepat. 404/405 yang terlihat acak sering membuat tim masuk ke area yang salah.
  • Mengandalkan satu jenis log. Anda butuh data dari edge, aplikasi, dan upstream.
  • Memperbaiki di satu lapisan saja. Misalnya menambah route alternatif untuk trailing slash, tetapi membiarkan proxy tetap mengubah path secara berbeda.
  • Menganggap semua normalisasi aman. Beberapa normalisasi mengubah arti path, bukan sekadar formatnya.
  • Tidak mendokumentasikan keputusan. Bug akan kembali saat ada migrasi proxy, upgrade framework, atau klien baru.

Checklist cepat untuk debugging API rapuh akibat normalisasi path

  1. Reproduksi request yang gagal dan berhasil dengan curl.
  2. Bandingkan path mentah di edge, aplikasi, dan upstream.
  3. Cek perbedaan trailing slash, slash ganda, dan URL-encoded segment.
  4. Pastikan route yang cocok dan method yang diizinkan benar-benar sama.
  5. Uji request yang sama melalui setiap jalur deployment yang relevan.
  6. Tentukan kontrak kanonis: terima, redirect, atau tolak.
  7. Selaraskan reverse proxy, framework, dan upstream.
  8. Tambahkan regression test dan observability untuk pola path bermasalah.

Penutup

Debug API rapuh: normalisasi path yang memicu 404 acak hampir selalu berujung pada satu pelajaran yang sama: path bukan string biasa. Sedikit perbedaan dalam slash, encoding, atau aturan routing dapat mengubah arti request secara fundamental.

Pendekatan terbaik bukan sekadar “membuat 404 hilang”, tetapi menetapkan kontrak path yang tegas, menyelaraskan perilaku di semua lapisan, dan memastikan bug serupa tertangkap lewat logging serta regression test. Dengan begitu, request yang tampak setara tidak lagi diperlakukan berbeda secara diam-diam.