← All writing

Technical note · 2026-09-14

Stream parsing: one network read is not one message

Separate byte decoding, event framing and application updates when reading streamed AI responses. Chunk boundaries must not decide message meaning.

Exploration · Principles and possible approaches inspired by a project context.

  • SSE
  • Streaming
  • UTF-8
  • Protocol parsing

Related project: AI agent application platform. This is an architectural exploration of its streaming context, not a claim that every failure below occurred in the project.

Reads and events have different boundaries

A transport chunk may contain several events or only part of one. Applying JSON.parse to each read accidentally treats transport boundaries as a message contract. A successful local test may simply have received convenient chunk sizes.

Event: data: {"text":"你好"}\n\n
Read 1: data: {"te
Read 2: xt":"你好"}\n\ndata: {"text":"next"}\n\n

Here \n represents actual newline characters in the stream.

Decode first, then frame events

UTF-8 characters can span byte chunks. Reuse a decoder for one response, enable streaming while more bytes are expected, and finish decoding at the end. See TextDecoder.decode.

SSE adds a separate framing layer: fields, line endings and blank lines. Multiple data: lines belong to one event; comments are not JSON payloads. Follow the SSE parsing specification.

Bytes → continuous text → complete events → application state
          decoder            parser             reducer

Native EventSource handles decoding and framing. A custom Fetch-based reader must provide the equivalent behavior when the application needs a different request mechanism or host integration.

Keep incomplete input without keeping everything

Retain the incomplete tail between reads. Remove complete events only after handing them to the application. Even a line-ending sequence may span chunks, so normalization cannot be limited to each individual read.

At the application layer, distinguish deltas, snapshots, errors and completion. SSE transports events; it does not define what those events mean to a conversation.

Bound the buffer. A server that never finishes an event can otherwise grow memory indefinitely. An agreed maximum event size needs an explicit failure path.

End of transport is not proof of success

Disconnection can leave half an event, half a character or a missing application completion signal. Represent an interrupted result separately from a successful one.

Standard SSE does not dispatch an unfinished final event without its terminating blank line. A service with different EOF rules needs an explicit protocol contract. Reconnection and deduplication remain a separate topic.

Test all possible cuts

Encode a known sequence, split it at different byte positions and compare the emitted events. Splitting inside Chinese characters, field names or separators must not change the meaning of complete input.

Include multiline data, comments, consecutive events, invalid application JSON, truncated endings and oversized events. The invariant is semantic consistency across transport chunking.