Debugging backend saat binary snapshot menyajikan aset web kedaluwarsa biasanya berujung pada satu pola: HTML berhasil dimuat, tetapi aset statis yang dirender pengguna tidak lagi cocok dengan versi aplikasi yang baru. Gejalanya sering membingungkan karena halaman tampak “terbuka”, namun styling rusak, JavaScript gagal jalan, atau gambar tertentu hilang. Dalam kasus layanan mirip Kage yang membungkus website menjadi single binary atau offline snapshot, bug ini sering baru muncul setelah deploy karena hasil crawl, aturan rewrite, cache lokal, dan manifest aset tidak lagi sinkron.

Jika Anda menangani backend untuk proses packaging semacam ini, masalahnya jarang murni di frontend. Sering kali akar masalah ada pada pipeline pengambilan aset, cara backend menulis ulang URL, validitas asset manifest, kebijakan cache, atau asumsi bahwa seluruh aset bisa ditemukan dari HTML awal. Artikel ini membahas studi kasus debugging yang realistis, langkah investigasi berurutan, serta perbaikan konkret agar snapshot tidak lagi menyajikan aset lama atau hilang.

Memahami pola bug pada layanan binary snapshot

Pada layanan yang mengubah website menjadi binary tunggal, alur umumnya seperti ini:

  1. Backend melakukan crawl ke satu atau beberapa entry URL.
  2. HTML dan aset statis yang ditemukan diunduh lalu disimpan ke paket lokal.
  3. Referensi asset path dapat diubah agar mengarah ke resource internal di binary.
  4. Saat binary dijalankan, pengguna melihat hasil snapshot alih-alih mengakses origin website langsung.

Masalah muncul ketika hasil crawl tidak merepresentasikan seluruh aset yang benar-benar dibutuhkan runtime. Pada aplikasi web modern, HTML awal sering hanya pintu masuk. CSS tambahan, JavaScript chunk, gambar hasil lazy-load, font, file manifest, dan data yang direferensikan secara dinamis bisa baru diminta setelah halaman berjalan. Jika proses bundling hanya mengandalkan HTML statis, snapshot bisa terlihat “berhasil” tetapi sebenarnya tidak lengkap.

Gejala khasnya: HTML versi baru tampil, tetapi file CSS/JS yang dipakai berasal dari snapshot lama, atau justru path asset mengarah ke nama fingerprint yang sudah tidak ada.

Gejala yang umum muncul setelah deploy

1. HTML terbuka, tetapi styling rusak

Biasanya browser berhasil memuat dokumen utama, namun file CSS gagal ditemukan atau versi CSS yang disajikan adalah hasil build sebelumnya. Hal ini sering terlihat sebagai layout tanpa gaya, komponen bertumpuk, atau ikon font tidak muncul.

2. JavaScript lama masih dipakai

Frontend dapat memuat bundle JavaScript yang berasal dari snapshot sebelumnya, sehingga kode runtime tidak cocok dengan HTML terbaru atau API response terbaru. Dampaknya bisa berupa error hydration, event handler tidak bekerja, atau blank screen setelah halaman tampak selesai dimuat.

3. Gambar atau font hilang sebagian

File-file ini sering tidak terambil saat bundling karena dimuat secara lazy, dihasilkan dari script, atau path-nya dibentuk secara dinamis. Di lingkungan development bug ini belum tentu terlihat karena origin server masih bisa menyuplai aset yang hilang, sedangkan setelah deploy binary dijalankan dalam mode terisolasi.

4. Bug hanya muncul setelah deploy

Ini petunjuk penting. Jika environment lokal berjalan normal, tetapi binary hasil deploy bermasalah, kemungkinan besar ada mismatch antara:

  • hasil crawl lokal vs hasil crawl di pipeline deploy,
  • asset manifest lama vs build baru,
  • cache service worker atau CDN vs aset internal binary,
  • aturan rewrite path saat build vs path yang dipakai saat runtime.

Root cause yang paling masuk akal

Path relatif vs absolut

Salah satu sumber bug klasik adalah perbedaan penanganan URL seperti ./app.css, /assets/app.css, assets/app.css, atau URL penuh ke domain origin. Saat backend mengunduh lalu menulis ulang referensi aset, path yang tadinya valid di origin belum tentu valid di dalam binary.

Contoh masalah umum:

  • Halaman di-serve dari path bersarang, misalnya /docs/page/, tetapi asset relatif dianggap berasal dari root.
  • Binary menjalankan file dari skema internal atau path virtual, sementara URL absolut masih menunjuk ke domain lama.
  • Rewrite mengganti sebagian path, tetapi melewatkan referensi di CSS seperti url(../fonts/font.woff2).

Fingerprint aset berubah

Banyak tool build frontend menghasilkan nama file berbasis hash, misalnya app.8f31c2.js. Ini benar untuk cache busting, tetapi menjadi masalah jika binary snapshot membawa HTML atau manifest dari versi baru sementara folder aset yang dibundel masih versi lama, atau sebaliknya.

Gejala khasnya:

  • HTML merujuk ke app.newhash.js, tetapi binary hanya menyimpan app.oldhash.js.
  • Manifest menyatakan chunk tertentu ada, namun file fisiknya tidak ikut tersalin.
  • Asset resolver di backend melakukan fallback ke file lama karena lookup file baru gagal.

Service worker dan cache policy

Website yang pernah memakai service worker dapat memunculkan perilaku yang terlihat seperti bug bundling, padahal file lama datang dari cache browser atau cache internal snapshot runtime. Jika service worker menyimpan shell aplikasi atau daftar aset lama, HTML baru bisa dipadukan dengan bundle JavaScript lama.

Selain itu, header seperti Cache-Control, ETag, dan Last-Modified perlu ditafsirkan hati-hati. Pada sistem snapshot, Anda bisa saja secara tidak sengaja mengawetkan respons yang seharusnya dianggap tidak valid setelah deploy baru.

Proses bundling tidak menarik aset dinamis

Ini akar masalah yang sangat sering terjadi pada layanan mirip Kage. Crawler mengambil apa yang tampak di HTML awal, tetapi melewatkan resource yang baru muncul setelah:

  • JavaScript dieksekusi,
  • route tertentu dibuka dari client-side router,
  • lazy-load atau intersection observer aktif,
  • request API mengembalikan URL aset tambahan,
  • manifest runtime memetakan chunk secara dinamis.

Akibatnya binary tampak utuh, namun aset untuk state tertentu hilang saat pengguna benar-benar memakai aplikasi.

Studi kasus: mismatch antara cache lokal, rewrite URL, dan asset manifest

Bayangkan backend memiliki pipeline sederhana:

  1. Crawl https://app.example.com/.
  2. Simpan HTML.
  3. Unduh semua <link>, <script>, dan <img> yang ditemukan.
  4. Tulis ulang URL menjadi path lokal internal, misalnya /__snapshot/assets/....
  5. Sajikan semuanya dari binary.

Pipeline ini terlihat cukup, tetapi bug muncul setelah deploy frontend baru:

  • Frontend build menghasilkan file hash baru.
  • Asset manifest di root berubah.
  • Crawler masih memakai cache lokal hasil run sebelumnya.
  • Beberapa path di HTML berhasil di-rewrite, tetapi path di CSS dan manifest runtime tidak.
  • Service worker pada mesin pengujian mengembalikan file lama sehingga hasil crawl tidak bersih.

Hasil akhirnya: HTML yang tersimpan mungkin sudah versi baru, tetapi sebagian aset yang dibundel masih versi lama. Karena binary memprioritaskan aset lokal, bug hanya muncul setelah deploy, saat origin dan snapshot tidak lagi identik.

Langkah investigasi berurutan

1. Reproduksi bug secara deterministik

Jangan mulai dari asumsi. Pastikan Anda bisa mereproduksi perilaku yang sama pada binary hasil deploy, lalu bandingkan dengan halaman asli dari origin.

  • Jalankan binary di environment bersih.
  • Gunakan profile browser baru atau nonaktifkan cache browser.
  • Jika perlu, uji juga tanpa koneksi ke origin untuk memastikan semua aset benar-benar berasal dari snapshot.

Tujuannya adalah memisahkan bug snapshot dari bug aplikasi asli.

2. Cek log fetch saat proses crawl dan saat runtime

Tambahkan logging pada backend bundler: URL apa yang diminta, status code, lokasi penyimpanan lokal, dan apakah respons datang dari cache atau jaringan. Logging ini sering lebih berguna daripada hanya melihat hasil akhir di browser.

[crawl] GET https://app.example.com/ -> 200 network
[crawl] GET https://app.example.com/assets/app.8f31c2.js -> 200 network
[crawl] GET https://app.example.com/assets/app.8f31c2.css -> 200 cache-local
[crawl] GET https://app.example.com/manifest.json -> 200 network
[rewrite] /assets/app.8f31c2.js -> /__snapshot/assets/app.8f31c2.js
[rewrite] /assets/app.8f31c2.css -> /__snapshot/assets/app.8f31c2.css

Dari log seperti ini Anda bisa mendeteksi dua hal penting:

  • Apakah crawler diam-diam memakai cache lokal lama.
  • Apakah URL yang disimpan dan URL yang di-rewrite memang konsisten.

3. Inspeksi asset manifest

Jika frontend memakai manifest build, cocokkan tiga sumber data berikut:

  1. Referensi file di HTML hasil crawl.
  2. Isi asset manifest.
  3. File fisik yang benar-benar dibundel ke binary.

Manifest yang tidak sinkron adalah indikator kuat. Misalnya, HTML menunjuk entry-client.abcd.js, manifest menunjuk chunk tambahan vendor.efgh.js, tetapi bundler hanya menyimpan entry file tanpa dependensi turunannya.

Saat inspeksi, perhatikan juga apakah backend membaca manifest versi yang sama dengan HTML. Dalam pipeline deploy paralel, sangat mungkin HTML berasal dari build terbaru tetapi manifest masih dari artefak lama.

4. Validasi header dan perilaku cache

Periksa apakah respons yang diambil crawler membawa header cache yang membuat backend menyimpan file lebih lama dari seharusnya. Fokus pada:

  • Cache-Control
  • ETag
  • Last-Modified
  • indikasi adanya service worker atau app shell caching

Jangan hanya melihat header dari origin. Periksa juga bagaimana runtime binary menyajikan ulang header tersebut. Menyalin header cache dari origin tanpa konteks bisa salah, karena file yang tadinya aman di-cache 1 tahun bergantung pada hash file yang benar-benar unik. Jika snapshot justru melayani file lama di path yang sama, cache panjang akan memperparah bug.

5. Bandingkan hasil crawl dengan halaman asli

Bandingkan output akhir secara sistematis, bukan sekadar visual:

  • Daftar script, stylesheet, gambar, dan font yang diminta origin.
  • Daftar file yang benar-benar tersimpan di binary.
  • Hash konten file penting.
  • Perbedaan URL setelah rewrite.

Bila perlu, buat diff sederhana antara hasil crawl dan dependency graph halaman asli. Ini membantu menemukan aset dinamis yang tidak pernah tertangkap crawler.

# Contoh pendekatan verifikasi
curl -s https://app.example.com/ > origin.html
curl -s http://localhost:8080/ > snapshot.html

# Lalu bandingkan referensi asset secara manual atau dengan parser HTML

Contoh sumber bug pada implementasi rewrite

Berikut contoh pseudo-code backend yang tampak benar, tetapi rawan mismatch:

for each assetURL in extractAssets(html) {
  localPath = download(assetURL)
  html = html.replace(assetURL, "/__snapshot/" + basename(localPath))
}

Masalah pada pendekatan ini:

  • Hanya menangani aset yang ditemukan langsung di HTML.
  • Mengabaikan referensi lanjutan dari CSS, JavaScript, dan manifest.
  • basename() bisa menyebabkan collision bila dua file berbeda punya nama sama di direktori berbeda.
  • Tidak mempertahankan struktur path sehingga URL relatif turunannya rusak.

Pendekatan yang lebih aman adalah mempertahankan struktur path virtual dan membangun dependency graph, bukan sekadar mengganti nama file.

queue = [entryURL]
visited = set()

while queue not empty:
  url = queue.pop()
  if url in visited:
    continue
  visited.add(url)

  response = fetch(url, bypass_cache=true)
  save_to_snapshot(preserve_path(url), response.body)

  refs = extract_references(response)
  for ref in refs:
    resolved = resolve_url(url, ref)
    if is_snapshot_candidate(resolved):
      queue.push(resolved)

Pendekatan ini masih sederhana, tetapi lebih realistis karena:

  • URL diselesaikan berdasarkan konteks dokumen asal.
  • Struktur path dipertahankan.
  • Traversal bisa diperluas untuk CSS, manifest, preload, dan resource lain.

Perbaikan konkret yang biasanya efektif

1. Paksa crawl bersih saat build snapshot

Jangan izinkan pipeline packaging memakai cache lokal tanpa kontrol. Untuk proses release, lebih aman melakukan fetch bersih atau setidaknya memisahkan cache per versi build.

  • Invalidasi cache lokal berdasarkan commit, build ID, atau versi artefak.
  • Jangan gunakan hasil crawl lama jika asset manifest berubah.
  • Pastikan mesin CI tidak mewarisi service worker atau cache browser dari run sebelumnya.

2. Sinkronkan HTML, manifest, dan folder aset sebagai satu unit artefak

Jika frontend membangun artefak sendiri, backend snapshot sebaiknya mengambil paket yang sudah konsisten, bukan mencrawl origin yang masih mungkin berubah di tengah proses deploy. Idealnya, HTML entry, manifest, dan direktori aset berasal dari artefak build yang sama.

Ini mengurangi race condition seperti:

  • HTML baru sudah ter-deploy, tetapi sebagian CDN edge masih menyajikan aset lama.
  • Manifest baru tersedia, tetapi file chunk belum tersalin seluruhnya.
  • Binary mengambil campuran versi selama window deploy.

3. Perbaiki resolver path dan aturan rewrite

Pastikan semua jenis URL ditangani konsisten:

  • path absolut dari root,
  • path relatif terhadap dokumen,
  • referensi dalam CSS,
  • URL yang berasal dari manifest atau preload,
  • base path aplikasi jika tidak berjalan di root domain.

Hindari rewrite destruktif yang mengubah struktur path terlalu agresif. Menjaga path virtual sering lebih aman daripada meratakan semua file ke satu direktori.

4. Tangkap aset dinamis yang diketahui

Jika aplikasi memuat chunk tambahan atau aset dari route tertentu, crawler perlu tahu entry point mana saja yang harus dikunjungi. Untuk aplikasi SPA, crawl satu halaman home biasanya tidak cukup.

Pilihan yang umum:

  • tambahkan daftar route penting ke proses snapshot,
  • parse manifest untuk menemukan seluruh dependensi entry,
  • jalankan browser headless agar JavaScript benar-benar dieksekusi sebelum daftar aset dikumpulkan.

Trade-off-nya, browser headless memberi cakupan lebih baik tetapi menambah kompleksitas dan waktu build.

5. Tinjau strategi service worker

Jika snapshot dipakai offline, service worker harus didesain sadar versi. Jangan biarkan service worker lama menangkap request ke aset snapshot baru. Pastikan ada mekanisme update, invalidasi cache, dan aktivasi versi yang eksplisit.

Bila service worker bukan kebutuhan inti untuk mode binary, pertimbangkan untuk menonaktifkannya pada snapshot agar satu lapisan cache bermasalah berkurang.

Checklist pencegahan sebelum deploy

  • HTML, asset manifest, dan seluruh file aset berasal dari artefak build yang sama.
  • Proses crawl release tidak memakai cache lokal lama tanpa namespacing versi.
  • Resolver URL telah diuji untuk path relatif, absolut, base path, dan referensi dalam CSS.
  • Traversal aset mencakup manifest, preload, font, gambar, dan resource yang dihasilkan runtime jika relevan.
  • Binary diuji dalam environment tanpa akses ke origin untuk mendeteksi aset yang masih bocor ke jaringan.
  • Header cache yang disajikan ulang oleh runtime binary sudah divalidasi.
  • Service worker diuji khusus atau dinonaktifkan pada mode snapshot jika tidak diperlukan.
  • Ada diff otomatis antara hasil crawl dan dependency graph halaman asli.

Pelajaran untuk developer backend

Kasus ini penting karena menunjukkan bahwa bug “aset frontend lama” sering bukan sekadar masalah frontend. Dalam layanan binary snapshot, backend bertanggung jawab atas konsistensi artefak, pengambilan resource, rewrite URL, dan kebijakan cache. Jika salah satu lapisan ini longgar, deploy akan menghasilkan kombinasi file yang sah secara individual tetapi rusak secara keseluruhan.

Pelajaran utamanya:

  • Jangan anggap HTML adalah sumber kebenaran tunggal. Aplikasi modern memiliki dependency graph yang lebih luas.
  • Jangan campur artefak lintas versi. Snapshot harus dibangun dari sumber yang konsisten.
  • Jangan percaya cache tanpa observabilitas. Log fetch dan metadata cache sangat penting.
  • Path handling adalah detail kecil dengan dampak besar. Salah resolve satu level direktori saja bisa membuat seluruh rantai aset gagal.

Untuk konteks layanan mirip Kage, pendekatan yang paling aman biasanya adalah memperlakukan proses packaging sebagai pipeline artefak yang deterministik, bukan sekadar crawler yang mengunduh apa yang terlihat. Dengan begitu, debugging backend saat binary snapshot menyajikan aset web kedaluwarsa menjadi lebih terukur: Anda bisa membuktikan file mana yang salah versi, siapa yang menulis ulang URL, cache mana yang ikut bermain, dan di titik mana aset dinamis tidak ikut terambil.