SSEStreamingJavaScriptTroubleshootingNbility

Why SSE Text Gets Corrupted: Byte Chunks Are Not Event Boundaries

A local example separates UTF-8 decoding, SSE framing, and application parsing, covering split characters, multiline data, comments, and incomplete events.

Why SSE Text Gets Corrupted: Byte Chunks Are Not Event Boundaries

Intermittent missing text, broken characters, or Unexpected end of JSON input can come from treating each network read as a complete message. An SSE event can span many reads, and a single UTF-8 character can span byte chunks. Changing models does not fix that boundary error.

This article was checked on November 10, 2026, against WHATWG, MDN, and eventsource-parser 4.1.3. The synthetic local example below was executed. It does not call a model or reproduce a particular proxy failure.

Decode bytes, assemble events, then interpret the protocol

These are three distinct stages. The network supplies bytes. A stateful UTF-8 decoder produces text. An SSE parser assembles events. Only then should the application interpret data according to its API protocol.

Bytes pass through continuous UTF-8 decoding, SSE framing, and application interpretation

Calling JSON.parse after every reader.read() can parse half a JSON object. Creating a new decoder for every chunk can corrupt a character split across chunks. One read can also contain the end of one event, several complete events, and the beginning of another.

The WHATWG specification defines UTF-8 decoding and line processing. Blank lines dispatch events, multiple data: lines are joined with newlines, and colon-prefixed lines are comments. CRLF and lone CR are valid line endings too, so splitting only on \n\n is insufficient.

A byte-at-a-time example

This fixture sends one byte per chunk, deliberately splitting characters, fields, and CRLF pairs. In a separate demo directory, install the dependency and use a modern Node.js runtime with the required Web Streams APIs:

npm install eventsource-parser@4.1.3
node demo.mjs

Save this as demo.mjs:

import assert from 'node:assert/strict';
import { EventSourceParserStream } from 'eventsource-parser/stream';

async function readEvents(response) {
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const type = response.headers.get('content-type')?.split(';')[0].trim();
  if (type !== 'text/event-stream' || !response.body) {
    throw new Error('Expected an SSE response body');
  }
  const reader = response.body
    .pipeThrough(new TextDecoderStream('utf-8', { fatal: true }))
    .pipeThrough(new EventSourceParserStream({ maxBufferSize: 65536 }))
    .getReader();
  const events = [];
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      events.push({ event: value.event || 'message', data: value.data });
    }
  } finally {
    await reader.cancel();
    reader.releaseLock();
  }
  return events;
}

const bytes = new TextEncoder().encode(
  ': keepalive\r\n\r\nevent: note\r\ndata: 你\r\ndata: 好\r\n\r\ndata: unfinished'
);
const body = new ReadableStream({
  start(controller) {
    for (const byte of bytes) controller.enqueue(Uint8Array.of(byte));
    controller.close();
  },
});
const response = new Response(body, {
  headers: { 'Content-Type': 'text/event-stream; charset=utf-8' },
});
const events = await readEvents(response);
assert.deepEqual(events, [{ event: 'note', data: '你\n好' }]);
console.log(events);

The local run produced one note event with data 你\n好. The assertion checks intact Unicode, multiline data assembly, ignored comments, and rejection of the unfinished event that lacks its terminating blank line.

This is a finite parsing demonstration. Its events array retains everything; a production consumer should process events incrementally and enforce connection deadlines, total deadlines, and output limits. maxBufferSize limits pending parser characters, not the total response size. See the library documentation for its API.

Framing success does not prove generation success

SSE does not require JSON data or define model completion. The fixture's data is simply a string.

After framing, check application data, the protocol's completion signal, and the task result separately

A model client must still interpret text deltas, tool calls, and terminal status using the selected API. Do not make one parser guess several protocols, or treat a matching string anywhere in data as a universal termination signal. The Chat Completions guide covers that interface; other protocols have their own event rules.

Check HTTP status and content type before parsing. An HTML page or error JSON under an unexpected response type needs a different path. Other causes of empty output are covered in empty-response troubleshooting.

Keepalives and reconnection need separate checks

MDN explains how comments can keep a connection active. A : keepalive comment demonstrates transport activity, not answer progress. Track those separately when diagnosing long-thinking timeouts.

A fetch consumer also does not acquire native browser EventSource reconnection behavior merely by using an SSE parser. Whether a new connection can resume an old request depends on the service protocol. Appending a newly generated answer to an interrupted answer can create a misleading composite result.

Preserve the failure position, verify byte handling, then check protocol completion and retry conditions

Start with this deterministic fixture, then try a finite, redacted sample. Once framing works, inspect terminal events, cancellation, and tool deltas. Use bounded retries only after deciding that a new request is appropriate. A timestamp, content type, last complete event type, and exception category are more useful for public debugging than a dump of private text or authorization headers.

Related posts

Calling Nbility from Node.js: From a CLI Tool to a Web App
Node.jsJavaScriptNbility

Calling Nbility from Node.js: From a CLI Tool to a Web App

A practical Node.js / JavaScript guide to using Nbility as an OpenAI-compatible API: environment variables, the openai npm SDK, CLI tools, Express endpoints, streaming output, error handling, logging, and launch checks.

Run your Agent workflow through Nbility

Get an API key and connect OpenAI-compatible models and developer tools from one place.

Get an API key