Pada layanan seperti tamnd/kage, klien mengirim URL untuk diproses menjadi snapshot yang bisa dilihat offline setelah JavaScript dihapus. Masalah utamanya bukan hanya cara memproses halaman, tetapi bagaimana mendesain kontrak API untuk snapshot web yang aman dari retry ganda ketika request timeout, jaringan putus, atau webhook gagal diterima.
Solusi yang paling aman adalah memperlakukan proses snapshot sebagai job asinkron yang idempotent. API perlu punya endpoint submit job, endpoint status job, callback/webhook hasil, autentikasi service-to-service, validasi URL yang ketat, dan aturan retry yang jelas. Tanpa itu, integrasi mudah menghasilkan job duplikat, status ambigu, atau callback palsu.
Masalah yang Harus Diselesaikan oleh Kontrak API
Untuk kasus snapshot web, retry adalah hal normal. Klien bisa mengulang request karena:
- Timeout di sisi klien, padahal server sebenarnya sudah menerima job.
- Koneksi putus setelah body terkirim tetapi sebelum respons diterima.
- Webhook hasil gagal diterima dan harus dikirim ulang.
- Queue internal gagal dan worker memproses ulang pesan.
Jika API hanya menerima POST /snapshots lalu langsung membuat job baru setiap kali request masuk, maka URL yang sama bisa diproses berkali-kali. Ini mahal, membingungkan, dan berisiko menghasilkan status akhir yang tidak konsisten.
Karena itu, kontrak API perlu menjawab beberapa pertanyaan sejak awal:
- Bagaimana klien mengirim job baru dengan aman?
- Bagaimana server mengenali retry dari request yang sama?
- Bagaimana status job dibaca tanpa polling yang ambigu?
- Bagaimana hasil akhir dikirim dan diverifikasi?
- Bagaimana error dibedakan antara yang bisa di-retry dan yang final?
Prinsip Desain Kontrak API
1. Submit selalu asinkron
Snapshot halaman web dapat memakan waktu lama karena fetch HTML, follow redirect, unduh aset, rewrite referensi, dan simpan artefak. Karena itu, endpoint submit sebaiknya hanya menerima job dan mengembalikan identitas job, bukan menunggu hasil selesai.
2. Idempotency wajib, bukan opsional
Request submit harus mendukung idempotency key. Klien mengirim key yang unik untuk satu niat bisnis, misalnya satu permintaan snapshot dari satu dokumen atau event tertentu. Bila request yang sama dikirim ulang, server harus mengembalikan job yang sama, bukan membuat job baru.
3. Status machine harus eksplisit
Status seperti queued, running, succeeded, failed, dan partial perlu didefinisikan dengan jelas. Hindari status kabur seperti processing jika transisinya tidak terukur.
4. Error model harus bisa dipakai klien untuk mengambil keputusan
Klien perlu tahu apakah suatu error harus diperbaiki input-nya, di-retry, atau dianggap permanen. Karena itu respons error sebaiknya punya code, message, dan bila perlu retryable.
5. Webhook tidak boleh dipercaya tanpa verifikasi
Hasil akhir melalui callback harus ditandatangani secara kriptografis, minimal dengan HMAC atas payload mentah dan timestamp. Tanpa ini, endpoint callback mudah dipalsukan.
Desain Endpoint yang Disarankan
Submit job
Endpoint utama:
POST /v1/snapshot-jobsHeader penting:
Authorization: Bearer <service-token>Idempotency-Key: <unique-key>Content-Type: application/jsonX-Request-Id: <trace-id>untuk observability
Contoh request:
{
"source_url": "https://example.com/articles/123",
"callback_url": "https://client.example.net/hooks/snapshot-result",
"deduplication_key": "article-123-v1",
"options": {
"max_redirects": 5,
"capture_assets": true,
"asset_size_limit_bytes": 5242880,
"page_size_limit_bytes": 10485760,
"timeout_seconds": 45
},
"metadata": {
"tenant_id": "acme",
"document_id": "123"
}
}Respons yang sehat untuk job baru:
{
"job_id": "job_01JABCDEFGH",
"status": "queued",
"source_url": "https://example.com/articles/123",
"deduplication_key": "article-123-v1",
"created_at": "2026-07-27T10:15:30Z",
"status_url": "/v1/snapshot-jobs/job_01JABCDEFGH"
}Status HTTP yang lazim dipakai adalah 202 Accepted untuk job baru. Jika request yang sama diulang dengan Idempotency-Key yang sama dan payload identik, server dapat mengembalikan respons yang sama dengan job lama.
Status job
Endpoint:
GET /v1/snapshot-jobs/{job_id}Contoh respons saat masih berjalan:
{
"job_id": "job_01JABCDEFGH",
"status": "running",
"source_url": "https://example.com/articles/123",
"created_at": "2026-07-27T10:15:30Z",
"started_at": "2026-07-27T10:15:35Z",
"progress": {
"stage": "fetch_assets",
"assets_discovered": 18,
"assets_downloaded": 11
}
}Contoh respons saat selesai dengan kegagalan parsial:
{
"job_id": "job_01JABCDEFGH",
"status": "partial",
"source_url": "https://example.com/articles/123",
"created_at": "2026-07-27T10:15:30Z",
"started_at": "2026-07-27T10:15:35Z",
"finished_at": "2026-07-27T10:16:12Z",
"result": {
"snapshot_url": "https://storage.example.com/snapshots/job_01JABCDEFGH/index.html",
"asset_summary": {
"downloaded": 25,
"failed": 3
}
},
"warnings": [
{
"code": "ASSET_FETCH_FAILED",
"message": "Beberapa aset gagal diunduh",
"details": {
"failed_urls": [
"https://cdn.example.com/app.js",
"https://img.example.com/banner.webp"
]
}
}
]
}Webhook hasil
Alih-alih hanya mengandalkan polling, klien sebaiknya menerima callback saat job berubah ke status terminal. Endpoint webhook berada di sisi klien, tetapi format payload perlu disepakati dalam kontrak.
Contoh payload webhook:
{
"event_id": "evt_01JHIJKLMN",
"event_type": "snapshot.job.completed",
"sent_at": "2026-07-27T10:16:13Z",
"job": {
"job_id": "job_01JABCDEFGH",
"status": "succeeded",
"source_url": "https://example.com/articles/123",
"deduplication_key": "article-123-v1",
"finished_at": "2026-07-27T10:16:12Z",
"result": {
"snapshot_url": "https://storage.example.com/snapshots/job_01JABCDEFGH/index.html"
}
}
}Header penting pada webhook:
X-Webhook-Id: identitas event unik untuk deduplikasi receiver.X-Webhook-Timestamp: timestamp saat signature dibuat.X-Webhook-Signature: HMAC signature.
Catatan: receiver webhook harus memperlakukan callback sebagai at-least-once delivery. Artinya event yang sama bisa diterima lebih dari sekali. Deduplikasi di sisi penerima tetap wajib.
Idempotency dan Deduplication Key: Jangan Disamakan
Dua konsep ini sering tercampur, padahal fungsinya berbeda.
Idempotency-Key
Idempotency-Key dipakai untuk memastikan retry dari request HTTP yang sama tidak membuat job baru. Scope-nya biasanya sempit: satu submit request, satu hasil job.
Aturan yang aman:
- Key wajib unik per niat submit.
- Server menyimpan pasangan
(client_id, idempotency_key). - Jika key yang sama datang lagi dengan payload identik, kembalikan job yang sama.
- Jika key yang sama datang dengan payload berbeda, balas error konflik karena itu indikasi bug klien.
Contoh error saat key sama tetapi body berbeda:
{
"error": {
"code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
"message": "Idempotency-Key sudah digunakan dengan payload berbeda",
"retryable": false
}
}Deduplication key
Deduplication key adalah identitas bisnis untuk mencegah snapshot yang secara semantik sama dibuat berkali-kali, walaupun request-nya berasal dari proses berbeda. Misalnya:
article-123-v1untuk snapshot dokumen versi tertentu.tenantA:https://example.com/a:2026-07-27untuk satu siklus ingest harian.
Perbedaannya:
- Idempotency-Key mencegah duplikasi akibat retry transport.
- deduplication_key mencegah duplikasi akibat operasi bisnis yang sama dikirim dari lebih satu alur.
Server bisa menerapkan kebijakan seperti:
- Jika ada job aktif dengan
deduplication_keyyang sama, kembalikan job aktif itu. - Jika ada job sukses terbaru dengan key yang sama dan masih dalam jendela reuse tertentu, kembalikan job lama.
- Jika pemanggil ingin memaksa proses ulang, sediakan flag eksplisit seperti
force_reprocess, bukan diam-diam membuat job baru.
Validasi URL: Wajib Ketat untuk Layanan Snapshot
Karena input utama adalah URL, validasi tidak boleh berhenti pada cek format string. Layanan snapshot berisiko menjadi pintu masuk SSRF, akses ke jaringan internal, atau fetch ke resource berbahaya.
Validasi minimum
- Hanya izinkan skema
httpdanhttps. - Tolak
file:,ftp:,data:,javascript:, dan skema lain. - Tolak URL tanpa host.
- Normalisasi host dan path sebelum hashing atau deduplikasi.
- Batasi panjang URL.
Proteksi SSRF
- Resolusi DNS harus diperiksa agar tidak mengarah ke alamat private, loopback, link-local, atau metadata service cloud.
- Validasi perlu dilakukan pada hasil resolusi awal dan idealnya pada koneksi akhir setelah redirect.
- Batasi jumlah redirect.
- Tolak redirect ke host atau skema yang tidak diizinkan.
Contoh error validasi:
{
"error": {
"code": "INVALID_SOURCE_URL",
"message": "URL tidak valid atau tidak diizinkan",
"retryable": false,
"details": {
"reason": "private_address_blocked"
}
}
}Perlu tidak allowlist domain?
Jika layanan hanya dipakai antar-sistem internal yang memproses domain tertentu, allowlist domain sering lebih aman daripada mencoba mendeteksi semua pola berbahaya. Trade-off-nya adalah fleksibilitas lebih rendah dan perlu proses operasional saat domain baru ditambahkan.
Autentikasi Service-to-Service
Untuk submit dan cek status, pendekatan paling sederhana adalah bearer token antar-layanan. Token bisa berupa token statis yang diputar berkala atau token yang berumur pendek dari sistem identitas internal. Terlepas dari mekanismenya, kontrak API sebaiknya mendefinisikan:
- Cara pengiriman token melalui header
Authorization. - Scope atau izin minimum, misalnya submit job dan read job.
- Identitas pemanggil yang akan diikat ke idempotency store dan rate limit.
Jangan hanya memakai IP allowlist sebagai autentikasi utama. IP bisa berubah, sulit diaudit, dan tidak memberi identitas request yang kuat.
Signed Webhook: Verifikasi Payload, Bukan Hanya Secret di Body
Webhook adalah titik integrasi yang rawan karena dipanggil dari luar sistem penerima. Pola yang umum dan cukup kuat adalah HMAC atas gabungan timestamp dan raw body.
Contoh skema header
X-Webhook-Timestamp: 1722075373
X-Webhook-Signature: v1=<hex-hmac>Cara verifikasi
- Ambil raw request body apa adanya sebelum parsing JSON.
- Bangun string yang ditandatangani, misalnya
{timestamp}.{raw_body}. - Hitung HMAC dengan shared secret.
- Bandingkan dengan signature menggunakan constant-time compare.
- Tolak jika timestamp terlalu tua untuk mencegah replay.
Contoh pseudocode:
signed_payload = timestamp + "." + raw_body
expected = HMAC_SHA256(webhook_secret, signed_payload)
if !constant_time_equal(expected, signature):
reject 401
if abs(now - timestamp) > allowed_skew:
reject 401Kesalahan umum:
- Menandatangani JSON setelah diparse lalu diserialisasi ulang; hasilnya bisa berubah.
- Tidak memeriksa timestamp sehingga replay attack tetap mungkin.
- Menganggap 200 dari receiver berarti event sudah benar-benar diproses, padahal bisa jadi hanya diterima.
Status Machine yang Jelas
Untuk layanan snapshot, state machine berikut cukup praktis:
queued: job diterima dan menunggu worker.running: worker sedang memproses.succeeded: snapshot utama selesai dan memenuhi kriteria sukses.partial: snapshot utama tersedia tetapi ada aset atau langkah non-kritis yang gagal.failed: job selesai gagal tanpa artefak yang layak dipakai.canceled: job dibatalkan secara eksplisit atau oleh kebijakan sistem.
Transisi yang sehat biasanya satu arah menuju status terminal:
queued -> running -> succeeded
queued -> running -> partial
queued -> running -> failed
queued -> canceled
running -> canceledHindari memindahkan job dari failed kembali ke running tanpa membuat attempt atau job baru yang jelas, karena ini menyulitkan audit dan debugging.
Mengapa status partial penting?
Dalam konteks snapshot web, kegagalan satu atau dua aset tidak selalu membuat seluruh hasil tidak berguna. Dengan status partial, klien bisa memutuskan apakah snapshot masih layak dipakai, perlu warning ke pengguna, atau harus dijadwalkan ulang.
Error Model yang Konsisten
Respons error sebaiknya konsisten di semua endpoint. Format sederhana:
{
"error": {
"code": "RATE_LIMITED",
"message": "Terlalu banyak request",
"retryable": true,
"details": {
"limit": "60/minute"
}
}
}Kategori error yang berguna:
- 4xx non-retryable: input salah, URL dilarang, callback URL invalid, idempotency conflict.
- 4xx retryable terbatas: rate limit, auth sementara gagal jika memang ada mode token refresh.
- 5xx retryable: kegagalan sementara server atau dependency.
Contoh code yang praktis:
INVALID_SOURCE_URLCALLBACK_URL_NOT_ALLOWEDUNAUTHORIZEDFORBIDDENRATE_LIMITEDIDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOADJOB_NOT_FOUNDREDIRECT_LOOPPAGE_TOO_LARGEDOMAIN_BLOCKEDUPSTREAM_TIMEOUT
Rate Limit dan Batasan Operasional
Layanan snapshot cenderung mahal karena melibatkan I/O jaringan, parsing HTML, dan penyimpanan aset. Karena itu kontrak API sebaiknya terbuka soal batasan.
Batasan yang layak didefinisikan
- Request submit per menit per klien.
- Jumlah job aktif bersamaan per klien.
- Batas ukuran HTML utama.
- Batas total aset atau total ukuran unduhan.
- Batas waktu pemrosesan per job.
- Batas jumlah redirect.
Jika rate limit diterapkan, respons sebaiknya memberi sinyal yang bisa dibaca mesin:
- Status
429 Too Many Requests. Retry-Afterbila memungkinkan.- Detail limit dalam body atau header.
Contoh:
{
"error": {
"code": "RATE_LIMITED",
"message": "Kuota submit job terlampaui",
"retryable": true,
"details": {
"retry_after_seconds": 30
}
}
}Edge Case yang Sering Terjadi
Redirect loop
URL bisa memantul tanpa akhir antar path atau domain. Kontrak API perlu menetapkan batas redirect dan error final yang jelas, misalnya REDIRECT_LOOP atau TOO_MANY_REDIRECTS. Ini biasanya non-retryable untuk URL yang sama sampai sumbernya diperbaiki.
Halaman sangat besar
Beberapa halaman memuat HTML atau aset sangat besar. Jika sistem punya batas ukuran, lebih baik gagal cepat dengan PAGE_TOO_LARGE daripada menghabiskan resource hingga timeout. Klien jadi tahu bahwa masalahnya ada di batas sistem, bukan gangguan sementara.
Domain terblokir
Domain dapat diblokir karena kebijakan keamanan, robots policy internal, atau daftar denylist. Ini harus muncul sebagai error eksplisit seperti DOMAIN_BLOCKED, bukan disamarkan sebagai timeout.
Partial failure asset
Snapshot utama mungkin berhasil, tetapi sebagian CSS, gambar, atau font gagal diunduh. Di sinilah status partial dan warnings berguna. Jangan sembunyikan kegagalan ini di log internal saja.
Retry webhook
Webhook harus di-retry jika receiver gagal merespons dengan status sukses. Tetapi retry webhook tidak boleh membuat event state baru. Gunakan event_id unik dan kirim payload yang sama untuk attempt ulang agar penerima dapat melakukan deduplikasi.
Pola Penyimpanan untuk Idempotency
Implementasi idempotency yang aman biasanya membutuhkan penyimpanan server-side. Bentuk sederhananya:
- Kunci:
(client_id, idempotency_key) - Nilai: hash payload,
job_id, status submit, timestamp expired
Alur umumnya:
- Request masuk.
- Autentikasi klien.
- Hitung fingerprint payload yang relevan, misalnya dari
source_url,callback_url,options, dandeduplication_key. - Lakukan operasi atomik untuk menyimpan pasangan key jika belum ada.
- Jika sudah ada, bandingkan fingerprint.
- Jika sama, kembalikan
job_idlama. - Jika berbeda, kembalikan konflik.
Yang penting adalah operasi atomik. Jika pengecekan dan penyimpanan dipisah tanpa lock atau primitive atomik, dua request paralel dengan key yang sama tetap bisa membuat dua job.
Polling vs Webhook
Keduanya bisa dipakai bersama, tetapi perannya berbeda:
- Polling lebih sederhana di sisi keamanan jaringan dan cocok sebagai fallback.
- Webhook lebih efisien untuk notifikasi hasil dan mengurangi polling agresif.
Pola yang umum:
- Submit job mengembalikan
job_iddanstatus_url. - Jika
callback_urldiberikan, server mengirim webhook saat status terminal. - Klien tetap bisa memanggil endpoint status untuk rekonsiliasi jika webhook terlambat atau gagal.
Contoh Kontrak Respons Terminal
Agar klien tidak perlu menebak, respons status terminal sebaiknya selalu memuat bentuk data yang konsisten.
{
"job_id": "job_01JABCDEFGH",
"status": "failed",
"source_url": "https://example.com/articles/123",
"created_at": "2026-07-27T10:15:30Z",
"started_at": "2026-07-27T10:15:35Z",
"finished_at": "2026-07-27T10:16:00Z",
"error": {
"code": "REDIRECT_LOOP",
"message": "URL berputar dalam redirect loop",
"retryable": false
}
}Dengan model ini, klien dapat membedakan:
- Job belum selesai: lihat field progres.
- Job selesai sukses: ambil
result.snapshot_url. - Job selesai partial: pakai hasil dengan warning.
- Job gagal: lihat code dan putuskan retry atau perbaikan input.
Debugging dan Kesalahan Umum
1. Idempotency key dibuat per retry
Jika klien membuat key baru tiap kali timeout, idempotency tidak berguna. Key harus tetap sama untuk retry dari niat submit yang sama.
2. Deduplikasi hanya berdasarkan URL mentah
URL yang sama bisa memiliki konteks berbeda, misalnya opsi timeout, batas aset, atau tenant berbeda. Karena itu deduplikasi bisnis sebaiknya mempertimbangkan scope yang tepat, bukan hanya string URL.
3. Tidak menyimpan raw body webhook
Tanpa raw body, verifikasi signature sering gagal atau menjadi tidak andal.
4. Status terminal tidak final
Jika job bisa berubah dari failed menjadi succeeded tanpa model attempt yang jelas, klien akan sulit merekonsiliasi state.
5. Error 500 untuk semua kasus
Jika semua masalah dibalas 500, klien akan terus retry termasuk untuk input yang jelas salah. Ini membuang resource kedua belah pihak.
Checklist Implementasi
- Definisikan
POST /v1/snapshot-jobssebagai endpoint submit asinkron. - Wajibkan
AuthorizationdanIdempotency-Key. - Simpan idempotency record secara atomik berdasarkan
(client_id, idempotency_key). - Tolak reuse key dengan payload berbeda.
- Dukung
deduplication_keyterpisah untuk mencegah duplikasi bisnis. - Normalisasi dan validasi
source_urldengan proteksi SSRF dan redirect policy. - Sediakan
GET /v1/snapshot-jobs/{job_id}dengan status machine yang eksplisit. - Gunakan status terminal:
succeeded,partial,failed,canceled. - Definisikan error model konsisten dengan
code,message, danretryable. - Dukung webhook hasil dengan signature HMAC, timestamp, dan
event_id. - Retry webhook dengan aman dan asumsikan delivery bisa duplikat.
- Publikasikan rate limit, batas ukuran halaman, batas aset, timeout, dan batas redirect.
- Catat
X-Request-Idatau correlation id untuk tracing end-to-end. - Pastikan observability: log submit, transisi status, webhook attempt, dan alasan kegagalan.
Penutup
Dalam layanan snapshot web seperti yang relevan dengan konteks tamnd/kage, kualitas integrasi lebih banyak ditentukan oleh desain kontrak API daripada sekadar worker yang bisa mengambil halaman. Jika submit job tidak idempotent, status tidak eksplisit, dan webhook tidak diverifikasi, retry normal dari jaringan akan berubah menjadi snapshot ganda, biaya ekstra, dan debugging yang melelahkan.
Desain yang aman biasanya sederhana: job asinkron, idempotency key, deduplication key, validasi URL yang ketat, state machine yang jelas, error model yang bisa ditindaklanjuti, dan webhook bertanda tangan. Dengan fondasi itu, klien dapat melakukan retry dengan percaya diri tanpa takut memicu pemrosesan ganda.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!