When you move generative AI features from a local prototype to a production workload handling thousands of concurrent users, the clean abstractions of standard REST APIs begin to leak.
Developers are taught to build against perfectly abstracted "Hello World" tutorials where network calls are instantaneous and LLMs return neat, semantic JSON objects. But underneath the hood, these systems are bound by the physical laws of distributed networks. You don't stream JSON; you stream raw, fragmented TCP byte frames.
The architectural gap and the two GenAI use cases
Before writing backend stream parsers, it is critical to determine if your architecture even requires one. Developers hit chunk boundary crashes when deploying backends for one of two distinct reasons:
Hiding the API key (the anti-pattern)
Many developers proxy LLM calls through a Cloud Function solely to hide their API keys from the client. If this is your only goal, deploying a backend proxy is an anti-pattern. You are taking on unnecessary latency, compute costs, and streaming headaches.
Trusted backend execution (the mandatory pattern)
If you are building a true agentic workflow (running Retrieval-Augmented Generation against a private vector database, executing tool calls to third-party endpoints, or hiding proprietary reasoning loops), that logic cannot live on the client.
If you fall into this second category, you are forced to intercept and parse the raw HTTP chunks manually in Node.js before streaming them back to the client. This is exactly where the abstraction breaks down.
The failure anatomy: UTF-8 and JSON tearing
When streaming LLM completions using Server-Sent Events (SSE) or HTTP chunked transfer encoding, your runtime receives binary Buffer or Uint8Array chunks. A common mistake is assuming each incoming chunk is a complete semantic token.
A. Multi-Byte UTF-8 Truncation: UTF-8 characters (IETF RFC 3629) span 1 to 4 bytes (e.g. ğ is 2 bytes, 🚀 is 4 bytes). When TCP packet boundaries (IETF RFC 9293) split across those bytes, calling chunk.toString('utf-8') on partial bytes produces \uFFFD (the Unicode Replacement Character), permanently corrupting the character.
B. Fragmented JSON Payloads: In structured output and tool-calling modes, models emit JSON frames inside SSE events. A single JSON object frequently spans multiple chunks. Calling JSON.parse(chunk) directly throws an immediate SyntaxError: Unexpected end of JSON input.
The old way vs. the new way
Here is how the naive parser compares to a production-ready stateful stream reassembler:
| Failure Mode | The Old Way (Naive Parser) | The New Way (Stateful Reassembler) |
|---|---|---|
| UTF-8 Byte Split | chunk.toString('utf-8') corrupts multi-byte sequences into \uFFFD. | StringDecoder('utf-8') holds incomplete bytes in internal buffer until complete. |
| Split JSON Payloads | JSON.parse(rawString) crashes runtime on partial frames. | Delimiter-based line buffering isolates complete data: blocks before parsing. |
| Fused SSE Packets | Processes first payload, dropping trailing events in same chunk. | Loops through all matched message delimiters (\n\n) within the combined buffer. |
| Error Recovery | Stream crashes, terminating client connection abruptly. | Emits parsing errors gracefully while keeping underlying transport alive. |
Accepts raw Buffer/Uint8Array network chunks, transparently retaining incomplete UTF-8 bytes in memory across TCP packet splits.
Accumulates decoded text until the canonical \n\n delimiter is found, slicing complete frames and preserving partial tails.
Executes isolated JSON parsing per block, gracefully falling back to raw text strings for unformatted tokens without aborting the stream.
Type text, shrink TCP chunks, watch UTF-8 characters tear at boundaries, then heal with streaming decode.
Production-ready TypeScript implementation
Below is the production-tested SafeSseReassemblyStream class. It drops in directly as a Node.js Transform stream on top of Cloud Functions or Firebase App Hosting endpoints:
// Resilient SSE & UTF-8 Stream Reassembler for Node.js / Cloud Run
import { StringDecoder } from 'node:string_decoder';
import { Transform, TransformCallback } from 'node:stream';
export interface SseEvent<T = unknown> {
event?: string;
data: T;
id?: string;
retry?: number;
}
export class SafeSseReassemblyStream extends Transform {
private readonly decoder: StringDecoder;
private buffer: string;
constructor() {
super({ readableObjectMode: true });
this.decoder = new StringDecoder('utf8');
this.buffer = '';
}
public _transform(
chunk: Buffer | Uint8Array | string,
_encoding: string,
callback: TransformCallback
): void {
try {
const text = typeof chunk === 'string'
? chunk
: this.decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
this.buffer += text;
let delimiterIndex: number;
while ((delimiterIndex = this.buffer.indexOf('\n\n')) !== -1) {
const rawEvent = this.buffer.slice(0, delimiterIndex);
this.buffer = this.buffer.slice(delimiterIndex + 2);
const parsed = this.parseSseBlock(rawEvent);
if (parsed) {
this.push(parsed);
}
}
callback();
} catch (error) {
callback(error instanceof Error ? error : new Error(String(error)));
}
}
public _flush(callback: TransformCallback): void {
try {
this.buffer += this.decoder.end();
if (this.buffer.trim().length > 0) {
const parsed = this.parseSseBlock(this.buffer);
if (parsed) {
this.push(parsed);
}
}
this.buffer = '';
callback();
} catch (error) {
callback(error instanceof Error ? error : new Error(String(error)));
}
}
private parseSseBlock(rawBlock: string): SseEvent | null {
const lines = rawBlock.split(/\r?\n/);
let eventType: string | undefined;
let id: string | undefined;
let retry: number | undefined;
const dataLines: string[] = [];
for (const line of lines) {
if (line.startsWith(':') || line.trim() === '') continue;
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;
const field = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).replace(/^\s/, '');
switch (field) {
case 'data': dataLines.push(value); break;
case 'event': eventType = value; break;
case 'id': id = value; break;
case 'retry': retry = Number.parseInt(value, 10); break;
}
}
if (dataLines.length === 0) return null;
const rawData = dataLines.join('\n');
let parsedData: unknown;
try {
const cleaned = rawData.trim();
const parsedData = cleaned.startsWith('{') || cleaned.startsWith('[')
? JSON.parse(cleaned)
: rawData;
} catch {
parsedData = rawData;
}
return {
event: eventType,
data: parsedData,
id,
retry,
};
}
}StringDecoder, decouple transport delimiters from business payloads, and your AI streaming architecture will never randomly crash in production.Primary research and documentation
- IETF RFC 9293: Transmission Control Protocol (TCP) Specification
- IETF RFC 3629: UTF-8, a transformation format of ISO 10646
- WHATWG HTML Standard: Server-Sent Events (SSE) Protocol
- Node.js StringDecoder Core API Specification
- Firebase AI Logic Documentation
- Firebase App Check Attestation
- Cloud Run Streaming and HTTP/2 Ingress
