Gangguan Sementara Model Claude

Model Claude Sementara Kita Off Kan Dulu Karena Tidak Stabil

← Semua artikel
Tutorial7 menit baca· 20 Oktober 2026

Streaming Respons AI dengan SSE di Browser

Cara membaca streaming SSE dari API AI di JavaScript: fetch + ReadableStream, penanganan potongan yang terputus, dan kesalahan umum yang membuat teks dobel.

Tanpa streaming, pengguna menunggu 5–15 detik melihat layar kosong. Dengan streaming, kata pertama muncul dalam 0,5 detik dan pengalaman terasa jauh lebih cepat — padahal total waktunya sama.

Bentuk datanya

Server mengirim potongan teks dengan format Server-Sent Events:

data: {"choices":[{"delta":{"content":"Halo"}}]}
data: {"choices":[{"delta":{"content":" dunia"}}]}
data: [DONE]

Tiga hal yang wajib diingat:

  1. Satu potongan belum tentu satu baris utuh — bisa terpotong di tengah JSON.
  2. Antar potongan dipisahkan \n\n, tapi ada juga keep-alive berupa baris kosong.
  3. Baris [DONE] menandakan selesai, bukan JSON.

Membaca stream di browser

js
export async function streamChat(messages, onToken) {
  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages, stream: true }),
  });

  if (!res.ok || !res.body) {
    throw new Error("Gagal: " + res.status + " " + (await res.text()));
  }

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    // proses per baris lengkap; sisa yang belum lengkap disimpan di buffer
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed.startsWith("data:")) continue;
      const payload = trimmed.slice(5).trim();
      if (payload === "[DONE]") return;
      try {
        const json = JSON.parse(payload);
        const text = json.choices?.[0]?.delta?.content;
        if (text) onToken(text);
      } catch {
        // potongan JSON belum utuh → biarkan, akan disusul di iterasi berikut
      }
    }
  }
}

Perhatikan decoder.decode(value, { stream: true }). Tanpa stream: true, karakter multi-byte (huruf beraksen, emoji, aksara non-Latin) bisa rusak di batas potongan.

Kesalahan yang membuat teks dobel

1. Menambahkan teks ke state lalu merender seluruh riwayat. Kalau state-nya array dan kamu push potongan, tapi render memakai join dari sumber lain, teks muncul dua kali. Simpan satu string yang di-append, atau satu array yang dirender sekali.

2. Memproses `buffer` yang belum lengkap. Memanggil JSON.parse pada potongan setengah jadi memang melempar error — dan kalau kamu mengabaikannya lalu mengulang dari buffer penuh, satu baris bisa diproses dua kali. Solusinya seperti kode di atas: simpan sisa, proses hanya baris lengkap.

3. Lupa menangani `abort`. Pengguna menutup tab atau menekan "stop", tapi request tetap jalan dan tetap memakan kuota. Pakai AbortController:

js
const ac = new AbortController();
fetch("/api/chat", { signal: ac.signal, /* … */ });
// tombol stop:
ac.abort();

Proxy di backend (jangan panggil provider dari browser)

Key tidak boleh ada di frontend. Alurnya: browser → backend-mu → provider. Backend meneruskan stream apa adanya:

js
// app/api/chat/route.js (Next.js App Router)
export async function POST(req) {
  const body = await req.json();
  const upstream = await fetch(process.env.AI_BASE_URL + "/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.AI_API_KEY}`,
    },
    body: JSON.stringify({ ...body, stream: true }),
  });

  return new Response(upstream.body, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
    },
  });
}

no-transform penting: sebagian proxy/CDN mengompres ulang dan menahan buffer, sehingga streaming terasa tidak jalan.

Ringkas

  • SSE = baris data: … dipisah baris kosong, diakhiri [DONE].
  • Selalu buffer sisa baris; parse hanya yang lengkap.
  • Pakai TextDecoder dengan stream: true.
  • Sediakan tombol stop dengan AbortController supaya kuota tidak terbuang.
  • Jangan pernah menaruh API key di browser.
Coba sendiri

Contoh di atas jalan di gateway ADVANCE — satu API key untuk GPT, Claude, dan Gemini, dengan hitungan kuota yang terbuka sebelum kamu bayar.

Baca juga