Client-side stream consumption

The server side streams events. Now you need a client that actually reads them as they arrive. curl with the right flag works for quick checks. Browsers need fetch with a ReadableStream reader because EventSource cannot send a POST body. Both patterns are short and reusable.

terminal
bash
# -N disables buffering so you see events as they arrive
curl -N -X POST http://localhost:8000/sentiment/analyze/stream \
  -H "Content-Type: application/json" \
  -d '{"text":"Great product, terrible support.","tasks":["sentiment","emotion","summary"]}'

The -N flag is mandatory. Without it, curl buffers the response and you see nothing until the stream closes, which defeats the whole purpose of streaming.

client.js
javascript
// EventSource only supports GET by default, so for POST bodies
// we use fetch with a ReadableStream reader.
async function streamAnalysis(text, tasks) {
  const res = await fetch("http://localhost:8000/sentiment/analyze/stream", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text, tasks }),
  });

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

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const events = buffer.split("\n\n");
    buffer = events.pop() || "";

    for (const raw of events) {
      if (!raw.startsWith("data: ")) continue;
      const event = JSON.parse(raw.slice(6));
      handleEvent(event);
    }
  }
}

function handleEvent(event) {
  switch (event.event) {
    case "start": console.log("Tasks queued:", event.tasks); break;
    case "task_start": console.log("Running:", event.task); break;
    case "task_done": console.log("Done:", event.task, event.result); break;
    case "task_error": console.warn("Failed:", event.task, event.error); break;
    case "done": console.log("Stream complete"); break;
  }
}

Split on \n\n to find event boundaries. Keep the trailing partial chunk in a buffer for the next read. This is the standard pattern for consuming SSE from fetch when EventSource is not an option.

EventSource only supports GET and cannot send a request body. Your analyze/stream endpoint takes a JSON body with the text and task list, so you need POST. The fetch + ReadableStream approach is the standard workaround. If your use case fits GET with query parameters, EventSource gives you automatic reconnection and simpler code.