Skip to content

Kontrak Endpoint Ingest yang Tahan Kegagalan Parsial

Adityo Guni Waluyo

Satu angka sukses untuk seluruh batch menyembunyikan kegagalan parsial. Kontrak per-item created/updated/unchanged/rejected membuatnya terlihat.

Ringkasan

Endpoint baru /v1/signals:ingest akhirnya nggak pakai satu status buat se-batch, tapi kasih status per item biar kegagalan parsial kelihatan. Tiap item bisa jadi created, updated, unchanged, atau rejected, plus ada pengaman kayak limit ukuran, jumlah item, dan rate limit. Autentikasi pakai Bearer token juga dibenerin, kalau token belum disetting server ngasih 503 bukan 401, dan semua udah dites live.

Ringkasan satu angka yang menyembunyikan kegagalan

Percobaan pertama ke endpoint baru berjalan persis seperti skenario yang tidak mau saya warisi: seluruh batch dianggap selesai oleh satu kode status, tanpa cerita per item. Padahal pada pipeline ingest, kegagalan parsial itu normal, sebagian item valid, sebagian ditolak karena format. Mengembalikan satu status sukses untuk seluruh batch adalah kebohongan di bawah kondisi itu: sistem pengirim tidak bisa membedakan keberhasilan total dari keberhasilan yang cacat.

Asumsi awal saya, validasi seluruh payload sebelum memproses satu pun item bisa menutup masalah. Untuk aliran data terus-menerus dari sumber eksternal, pendekatan all-or-nothing itu justru memindahkan masalah: satu item buruk memblokir ratusan item valid. Bentuk yang jujur adalah status independen per item, ditambah ringkasan di tingkat batch.

Commit ini membangun kontrak tersebut di POST /v1/signals:ingest untuk DemandScope, lengkap dengan pengaman dan semantik per item, lalu mengujinya live: tanpa token menghasilkan 401, token benar menciptakan data baru, kirim ulang payload identik dianggap tak berubah, dan payload yang berubah tercatat sebagai pembaruan.

Rute custom method dan autentikasi yang gagal tertutup

Sufiks titik dua pada /v1/signals:ingest mengikuti konvensi custom method: operasi yang tidak dipetakan rapi ke verba CRUD standar diberi ruang sendiri supaya kosakata API mengikuti niat pengguna [11]. Alias /v1/signals/ingest disediakan untuk HTTP stack yang merusak jalur bertitik dua. Keduanya memanggil handler yang sama.

Autentikasi memakai header Authorization: Bearer sesuai konvensi yang didokumentasikan framework [9]. Keputusan yang paling sering ditanyakan: mengapa token yang belum dikonfigurasi menghasilkan 503, bukan 401. Alasannya balik ke makna kode status. 401 berarti permintaan tidak punya kredensial yang sah [7], sedangkan 503 berarti server sedang tidak mampu menangani permintaan karena kondisi di sisinya dan kemungkinan pulih setelah beberapa saat [7]. Server yang lupa dikonfigurasi tokennya adalah kondisi sisi server, bukan kesalahan pemanggil, jadi endpoint gagal tertutup dengan 503. Perbandingan token sendiri memakai secrets.compare_digest, fungsi perbandingan constant-time yang dirancang menekan risiko timing attack [8].

Pengaman permintaan: ukuran, jumlah, laju

Tiga pagar menjaga endpoint ini. Pertama, cap 1000000 byte lewat pemeriksaan content-length; permintaan yang lebih besar ditolak dengan 413 Content Too Large, kode untuk konten yang melampaui kemauan atau kemampuan server memprosesnya [7]. Kedua, maksimum 100 item per batch. Ketiga, rate limit per sumber 60 permintaan per menit; pelampauannya menjawab 429 Too Many Requests, kode yang diperkenalkan RFC 6585 untuk klien yang mengirim terlalu banyak permintaan dalam selang waktu tertentu [6], lengkap dengan header Retry-After supaya pengirim tahu kapan boleh melanjutkan.

Semua penolakan di atas terjadi sebelum satu pun item diproses. Itu disengaja: pengaman yang murah dan diletakkan di depan jauh lebih baik daripada membersihkan data setengah masuk.

Semantik per item yang dihitung di sisi server

Inti kontraknya: setiap item mendapat salah satu dari empat status, dihitung di sisi server dari content hash kanonik yang dikunci pada pasangan (source_id, source_ref) sejak migrasi 0003. Statusnya bernama created untuk item baru, updated untuk perubahan nyata, unchanged saat konten identik dikirim ulang sehingga hanya waktu terakhir dilihat yang maju, dan rejected untuk item yang gagal validasi tanpa menyeret item lain. Setiap permintaan mencatat satu baris IngestRun sebagai jejak audit.

Bentuk responsnya bisa dicoba sendiri:

curl -X POST https://example.com/v1/signals:ingest \
  -H "Authorization: Bearer " \
  -H "Content-Type: application/json" \
  -d '{"source_type": "sipp", "items": [{"dedupe_key": "item-001"}]}'

# Respons:
# {
#   "results": [{"source_ref": "item-001", "status": "created"}],
#   "summary": {"seen": 1, "created": 1, "updated": 0, "unchanged": 0, "rejected": 0}
# }

Dengan struktur ini, pengirim bisa memicu alarm hanya pada item rejected, memantau rasio unchanged sebagai sinyal pipeline yang sehat, dan tidak pernah menebak-nebak apa yang sebenarnya tersimpan. Sebelas tes endpoint baru mengunci perilaku ini, dari 401 sampai siklus created-unchanged-updated.

Pengalaman dari commit ini meninggalkan satu prinsip kerja untuk endpoint batch mana pun: jangan pernah membalas batch dengan satu angka. Granularitas status per item bukan kemewahan, melainkan syarat supaya kegagalan parsial bisa dilihat, diukur, dan dipulihkan secara otomatis.

Sumber

  1. RFC 6585: Additional HTTP Status Codes
  2. RFC 9110: HTTP Semantics
  3. Python docs: secrets
  4. FastAPI: Security first steps
  5. AIP-136: Custom methods

Artikel terkait