Go Fiber: Debug SSR State Drift pada HTML dari API Template sering muncul dalam bentuk UI yang membingungkan: HTML awal terlihat benar saat respons diterima, tetapi beberapa saat kemudian teks, badge, filter, tanggal, atau jumlah item berubah sendiri ketika JavaScript di browser mulai berjalan. Ini mirip hydration mismatch, meskipun Anda tidak memakai framework frontend SSR.
Penyebab utamanya biasanya sederhana tetapi sulit terlihat: state yang dipakai server saat merender template tidak identik dengan state awal yang dipakai JavaScript di browser. Pada stack Go Fiber, masalah ini sering datang dari data API yang dibaca dua kali, default state yang berbeda, perbedaan timezone/locale, atau serialisasi JSON yang tidak aman di dalam template.
Masalah yang Sebenarnya: Bukan Hydration Framework, tetapi State Drift
Pada aplikasi berbasis Go Fiber dengan html/template, pola yang umum adalah:
- Server memanggil API atau service internal.
- Server merender HTML menggunakan data tersebut.
- Server juga menyuntikkan initial state ke dalam halaman agar JavaScript di browser bisa melanjutkan interaksi.
- Setelah halaman dimuat, JavaScript membaca state awal atau memanggil API lagi.
Jika data pada langkah render HTML tidak sama dengan data yang dipakai JavaScript setelahnya, terjadilah SSR state drift: DOM awal dan state runtime bergerak ke arah berbeda.
Gejalanya bisa berupa:
- Jumlah item di badge berubah setelah halaman muncul.
- Status active/inactive berbeda antara HTML awal dan komponen interaktif.
- Tanggal yang awalnya benar berubah satu hari maju atau mundur.
- Tombol terpilih, tab aktif, atau filter default berubah sendiri.
- Daftar tampak terurut pada HTML awal, lalu urutannya berubah di browser.
- Teks dengan karakter khusus rusak karena JSON di template tidak di-escape dengan benar.
Jika bug terlihat seperti “flash of wrong state”, “UI lompat”, atau “server bilang A tetapi browser menjalankan B”, fokuskan investigasi pada konsistensi data lintas boundary: service, template, JSON injection, dan JavaScript bootstrap.
Contoh Kasus Nyata pada Go Fiber
Misalkan halaman daftar pesanan dirender oleh Go Fiber. Server menampilkan 10 pesanan terbaru dan status filter open. Di sisi browser, JavaScript membaca initial state untuk mengaktifkan pencarian dan pagination.
Bug muncul ketika:
- HTML server dirender dari hasil API pada pukul 23:59 UTC.
- JavaScript di browser memformat ulang tanggal memakai timezone lokal pengguna.
- Filter default di JavaScript adalah
all, sementara server merenderopen. - Atau browser melakukan fetch ulang ke API yang datanya sudah berubah beberapa milidetik atau detik kemudian.
Akibatnya pengguna melihat satu hasil pada render awal, lalu hasil lain sesaat setelah JavaScript aktif. Secara visual, ini terasa seperti mismatch render.
Akar Masalah yang Paling Sering Terjadi
1. Data race antar sumber data
Masalah paling umum adalah server mengambil data untuk HTML dan JavaScript mengambil data lagi dari endpoint yang sama atau berbeda, tetapi tidak pada momen yang sama. Jika data cepat berubah, dua snapshot itu berbeda.
Contoh:
- Server render memakai query
GET /orders?status=open. - Browser setelah load memanggil lagi endpoint tersebut.
- Di antara dua panggilan itu, ada pesanan baru atau perubahan status.
Ini bukan race di level goroutine saja, tetapi juga race di level time-of-read.
2. Default state server dan browser tidak sama
Sering terjadi ketika HTML dihitung dari parameter request, tetapi JavaScript memulai state dengan nilai bawaan yang hard-coded.
Contoh:
- Server: filter default =
open. - Browser: filter default =
all. - Server: sort =
created_at desc. - Browser: sort =
name asc.
Jika state awal tidak berasal dari sumber yang sama, mismatch hampir pasti terjadi.
3. Timezone dan locale
Tanggal dan angka adalah sumber mismatch klasik. Server bisa merender waktu dalam UTC atau timezone server, sedangkan browser menampilkan dengan timezone lokal pengguna. Format locale juga bisa berbeda, misalnya:
- Server:
01/02/2025dengan asumsi DD/MM/YYYY. - Browser: menafsirkan sebagai MM/DD/YYYY.
- Server merender jam tanpa offset, browser membuat objek
Datedengan asumsi berbeda.
Hasilnya bukan hanya format berbeda, tetapi bisa bergeser hari.
4. Escaping JSON di template yang salah
Banyak bug berasal dari injeksi JSON ke dalam <script> tanpa serialisasi yang benar. Karakter kutip, newline, tag penutup script, atau karakter khusus HTML dapat merusak payload. JavaScript lalu membaca state yang berbeda atau bahkan gagal parse.
Contoh buruk:
<script>
window.__INITIAL_STATE__ = {{ .State }}
</script>Jika .State bukan JSON yang sudah diserialisasi dengan benar, output bisa invalid atau ter-escape tidak sesuai kebutuhan.
5. Transformasi data server dan browser tidak identik
Server mungkin sudah menghitung field turunan seperti label status, grouping, urutan, atau summary count. Di browser, perhitungan ulang dilakukan dengan aturan berbeda.
Contoh:
- Server menghitung badge dari item yang visible setelah filter.
- Browser menghitung badge dari semua item hasil fetch.
- Server membulatkan angka ke bawah, browser memakai round half up.
Pola Implementasi yang Aman di Go Fiber
Prinsip paling penting adalah: render HTML dan initial state harus berasal dari snapshot data yang sama. Jika memungkinkan, lakukan satu kali pengambilan data di server, lalu gunakan hasil itu untuk:
- render template HTML, dan
- menyuntikkan state awal yang akan dipakai JavaScript.
Berikut contoh sederhana dengan Go Fiber dan html/template.
package main
import (
"context"
"encoding/json"
"html/template"
"log"
"time"
"github.com/gofiber/fiber/v2"
)
type Order struct {
ID string `json:"id"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
type PageState struct {
Filter string `json:"filter"`
Timezone string `json:"timezone"`
Generated string `json:"generated"`
Orders []Order `json:"orders"`
}
type PageData struct {
Title string
Orders []Order
InitialStateJSON template.JS
}
func fetchOrders(ctx context.Context, filter string) ([]Order, error) {
return []Order{
{ID: "ORD-1001", Status: "open", CreatedAt: time.Now().UTC()},
{ID: "ORD-1002", Status: "open", CreatedAt: time.Now().UTC().Add(-2 * time.Hour)},
}, nil
}
func main() {
app := fiber.New()
tmpl := template.Must(template.ParseFiles("views/orders.html"))
app.Get("/orders", func(c *fiber.Ctx) error {
filter := c.Query("status", "open")
orders, err := fetchOrders(c.Context(), filter)
if err != nil {
return fiber.ErrInternalServerError
}
state := PageState{
Filter: filter,
Timezone: "UTC",
Generated: time.Now().UTC().Format(time.RFC3339),
Orders: orders,
}
raw, err := json.Marshal(state)
if err != nil {
return fiber.ErrInternalServerError
}
data := PageData{
Title: "Orders",
Orders: orders,
InitialStateJSON: template.JS(raw),
}
c.Type("html", "utf-8")
return tmpl.Execute(c.Response().BodyWriter(), data)
})
log.Fatal(app.Listen(":3000"))
}Template HTML:
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }}</title>
</head>
<body>
<h1>Daftar Pesanan</h1>
<ul id="orders">
{{ range .Orders }}
<li data-id="{{ .ID }}">
<strong>{{ .ID }}</strong> - {{ .Status }} - {{ .CreatedAt.Format "2006-01-02 15:04:05 MST" }}
</li>
{{ end }}
</ul>
<script>
window.__INITIAL_STATE__ = {{ .InitialStateJSON }};
</script>
<script src="/static/orders.js" defer></script>
</body>
</html>Poin penting dari contoh di atas:
- Data untuk HTML dan JavaScript berasal dari hasil
fetchOrdersyang sama. - State diserialisasi dengan
json.Marshal, bukan dirakit manual dengan string. - Timezone state dinyatakan eksplisit, tidak dibiarkan implisit.
- Filter awal mengikuti query request, bukan default hard-coded yang terpisah.
Catatan penting soal keamanan injeksi JSON
Penggunaan template.JS harus hati-hati. Aman hanya jika nilainya memang hasil serialisasi JSON yang valid dari data terpercaya atau data yang sudah diproses dengan benar. Jangan membungkus string mentah dari user input sebagai template.JS.
Alternatif yang lebih defensif adalah menaruh JSON dalam tag non-executable:
<script id="initial-state" type="application/json">{{ .InitialStateJSON }}</script>Lalu di browser:
const el = document.getElementById('initial-state');
const state = JSON.parse(el.textContent);Pendekatan ini sering lebih mudah di-debug karena Anda bisa memeriksa payload persis seperti yang tertulis di HTML.
Alur Debug Langkah demi Langkah
Untuk debug Go Fiber: Debug SSR State Drift pada HTML dari API Template, jangan mulai dari browser saja. Lakukan pembuktian berurutan dari server ke DOM, lalu ke JavaScript runtime.
1. Simpan respons HTML mentah
Gunakan DevTools Network atau curl untuk mengambil HTML mentah sebelum JavaScript berjalan.
curl -s http://localhost:3000/orders?status=open > response.htmlPeriksa dua hal:
- Apakah daftar/order pada HTML sesuai dengan yang Anda harapkan?
- Apakah payload
window.__INITIAL_STATE__atauapplication/jsonsama dengan yang dipakai render server?
Jika HTML dan initial state sudah berbeda di respons mentah, bug ada di server/template, bukan di browser.
2. Bandingkan data render dan initial state di server log
Tambahkan log terstruktur sementara untuk request yang bermasalah. Log yang berguna:
- request id
- query parameter
- snapshot waktu data diambil
- jumlah item untuk HTML
- jumlah item dalam initial state
- timezone/locale yang dipakai
Tujuannya memastikan HTML dan state dibangun dari snapshot yang sama.
3. Bekukan sumber data sementara
Jika datanya dinamis, debugging akan sulit. Coba:
- gunakan fixture statis,
- matikan polling/fetch ulang di browser,
- atau pin response API melalui mock.
Kalau mismatch hilang saat data dibekukan, kemungkinan besar masalahnya adalah race antar snapshot.
4. Verifikasi bootstrap JavaScript
Di browser, cek apakah state awal yang dibaca JavaScript sama dengan yang tertulis di HTML.
console.log(window.__INITIAL_STATE__);Atau jika memakai tag JSON:
const raw = document.getElementById('initial-state').textContent;
console.log(raw);
console.log(JSON.parse(raw));Jika parse gagal atau bentuk datanya berubah, investigasi escaping dan encoding karakter khusus.
5. Cek transformasi setelah load
Sering kali mismatch bukan pada data mentah, tetapi pada transformasi klien. Audit bagian yang melakukan:
- sorting,
- filtering,
- formatting tanggal,
- normalisasi angka,
- grouping,
- deduplikasi.
Bandingkan hasil transformasi server dan browser dengan input yang sama.
6. Audit timezone dan locale secara eksplisit
Jangan mengandalkan default lingkungan. Log nilai:
- timezone server,
- format timestamp yang dikirim,
- locale browser,
- timezone browser.
Praktik aman:
- kirim timestamp dalam format RFC3339 atau Unix time,
- simpan sumber kebenaran dalam UTC,
- format tampilan di satu tempat dengan aturan yang jelas.
7. Deteksi fetch ulang yang tidak perlu
Banyak UI melakukan fetch setelah load meskipun data awal sudah ada. Pastikan ada keputusan yang jelas:
- pakai initial state tanpa refetch langsung, atau
- refetch memang diperlukan karena data harus selalu fresh.
Jika memilih refetch, Anda harus siap menerima perbedaan state dan mendesain UI untuk itu, misalnya dengan indikator refresh, timestamp, atau merge strategy yang eksplisit.
Contoh Root Cause dan Perbaikannya
Kasus 1: Server dan browser memakai default filter berbeda
Gejala: halaman awal menampilkan item open, lalu berubah menjadi semua item.
Akar masalah: server membaca query status dengan default open, JavaScript menginisialisasi state dengan all.
Perbaikan: jadikan state server sebagai satu-satunya sumber kebenaran saat bootstrap. Browser harus membaca filter dari initial state, bukan dari konstanta lokal.
Kasus 2: Tanggal bergeser satu hari
Gejala: server merender 2025-02-01, browser menampilkan 2025-01-31 untuk sebagian user.
Akar masalah: server mengirim string tanggal tanpa offset atau tanpa waktu, browser menafsirkannya dengan timezone lokal.
Perbaikan: kirim timestamp eksplisit, misalnya RFC3339 UTC, dan formatkan dengan aturan yang konsisten.
Kasus 3: JSON initial state rusak untuk karakter tertentu
Gejala: bug hanya muncul saat nama item mengandung kutip, newline, atau karakter khusus.
Akar masalah: JSON dibentuk manual di template atau di-concatenate sebagai string JavaScript.
Perbaikan: selalu gunakan json.Marshal. Hindari merakit object JavaScript secara manual dari potongan string template.
Kasus 4: Browser melakukan refetch terlalu cepat
Gejala: daftar awal benar, lalu berubah dalam 100-500 ms.
Akar masalah: script langsung memanggil API ulang saat DOMContentLoaded, sementara data sudah berubah.
Perbaikan: gunakan initial state sebagai sumber render pertama. Refetch hanya bila perlu dan tampilkan bahwa data sedang disegarkan.
Checklist Verifikasi sebelum Menyimpulkan Bug Selesai
- HTML awal dan initial state berasal dari snapshot data yang sama.
- Server dan browser memakai default filter/sort/page yang sama.
- Timestamp dikirim dalam format eksplisit, idealnya UTC atau format yang jelas offset-nya.
- Formatting tanggal/angka tidak mengubah makna data dasar.
- JSON initial state dibuat dengan serializer, bukan string manual.
- Karakter khusus seperti kutip, newline, dan
</script>tidak merusak payload. - Tidak ada refetch otomatis yang tidak disadari pada saat bootstrap.
- Log request id bisa menghubungkan render server dengan perilaku browser.
- Transformasi seperti sort/filter/count identik di server dan browser, atau hanya dilakukan di satu sisi.
- Perubahan state setelah load memang disengaja dan dikomunikasikan ke pengguna.
Strategi Pencegahan
1. Gunakan satu sumber kebenaran untuk render awal
Data yang dipakai membuat HTML harus persis sama dengan data bootstrap JavaScript. Idealnya dari satu struct Go yang sama.
2. Minimalkan duplikasi logika antara server dan browser
Semakin banyak transformasi dilakukan di dua tempat, semakin besar peluang drift. Jika mungkin:
- server menyiapkan data final untuk render awal,
- browser hanya menambahkan interaksi, bukan menghitung ulang seluruh state.
3. Tetapkan kontrak state yang eksplisit
Definisikan field state secara jelas: filter, sort, locale, timezone, timestamp, dan data utama. Jangan biarkan browser menebak nilai yang tidak dikirim.
4. Perlakukan timezone dan locale sebagai bagian dari data
Bukan detail presentasi semata. Jika UI sensitif terhadap tanggal/jam, kirim konteksnya secara eksplisit.
5. Tambahkan snapshot test atau golden test untuk HTML
Pada halaman kritis, uji output HTML dan payload initial state untuk input tertentu. Ini membantu menangkap perubahan tidak sengaja pada template atau serializer.
6. Instrumentasi untuk mismatch yang sulit direproduksi
Untuk bug sporadis di produksi, simpan:
- request id,
- hash initial state,
- waktu render,
- versi asset JavaScript,
- parameter query penting.
Dengan begitu Anda bisa membandingkan apa yang dirender server dan apa yang dipakai browser saat bug dilaporkan.
Kapan Refetch Setelah Render Awal Masih Masuk Akal?
Ada kasus di mana refetch tetap benar, misalnya dashboard real-time atau status yang cepat berubah. Namun trade-off-nya harus jelas:
- Kelebihan: data lebih segar.
- Kekurangan: risiko UI meloncat, perbedaan count, dan pengalaman pengguna yang terasa tidak stabil.
Jika Anda butuh refetch:
- tampilkan indikator refresh,
- bedakan state awal dengan state terbaru,
- hindari menimpa pilihan user secara diam-diam,
- pertimbangkan merge yang lebih halus daripada rerender penuh.
Penutup
Bug yang terlihat seperti hydration mismatch pada Go Fiber sering bukan masalah framework frontend, melainkan state drift antara HTML hasil render server dan state awal yang dipakai JavaScript di browser. Kunci penyelesaiannya adalah memastikan snapshot data konsisten, default state tidak bercabang, timezone/locale eksplisit, dan JSON di template disuntikkan dengan aman.
Jika Anda menemui UI yang “benar sesaat lalu berubah sendiri”, mulai dari membandingkan raw HTML response, payload initial state, dan transformasi browser. Dalam banyak kasus, root cause akan terlihat begitu Anda membuktikan bahwa server dan browser ternyata memulai dari dua versi kebenaran yang berbeda.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!