Skip to content

Fix multi-byte UTF-8 corruption in streaming Ask responses - #228

Open
surgaev wants to merge 1 commit into
pickle-com:mainfrom
surgaev:fix/streaming-multibyte-corruption
Open

Fix multi-byte UTF-8 corruption in streaming Ask responses#228
surgaev wants to merge 1 commit into
pickle-com:mainfrom
surgaev:fix/streaming-multibyte-corruption

Conversation

@surgaev

@surgaev surgaev commented Aug 9, 2026

Copy link
Copy Markdown

What

askService.js's _processStream reads the streaming response body chunk-by-chunk and calls chunk.split('\n') directly on each decoded chunk to find SSE lines.

Two related bugs:

  1. Multi-byte UTF-8 corruption: TextDecoder.decode() was called without { stream: true }, so a multi-byte UTF-8 character (e.g. Cyrillic, CJK, emoji) split across two network chunk boundaries gets decoded incorrectly — each half is decoded independently instead of being buffered until a full character is available, producing garbled/replacement characters () in streamed output.
  2. SSE line-splitting across chunks: a single SSE data: ... line isn't guaranteed to arrive in one chunk. Splitting each chunk on \n independently (instead of maintaining a persistent line buffer across chunks) can silently drop or merge lines whenever a line boundary doesn't line up with a chunk boundary.

Users streaming Ask responses in any non-ASCII-heavy language (Russian, Chinese, Japanese, etc.) intermittently see corrupted characters mid-response, and occasionally a line gets dropped entirely.

Fix

  • Added a persistent lineBuffer that accumulates decoded text across chunks and only processes complete lines (splitting on the last \n), carrying any trailing partial line over to the next chunk.
  • Changed decoder.decode(value) to decoder.decode(value, { stream: true }), which tells TextDecoder to hold back a trailing incomplete multi-byte sequence and prepend it to the next decode() call instead of emitting a corrupted character immediately.

Testing

Verified locally with a Russian-language conversation (OPENAI_TRANSCRIBE_LANG=ru / Russian prompts) streamed through Ask: previously-frequent corruption in streamed responses no longer occurs, and long responses no longer show dropped/merged lines.

_processStream() had two related bugs in how it consumed the SSE
response stream:

1. decoder.decode(value) was called without { stream: true }. When a
   multi-byte UTF-8 character (e.g. Cyrillic, or any non-ASCII text)
   happened to be split across two network chunks, TextDecoder would
   mangle or drop the incomplete sequence instead of buffering it,
   since it wasn't told the stream was still ongoing.

2. Each chunk was decoded and split on '\n' independently
   (chunk.split('\n')), with no buffering across reader.read() calls.
   If a single `data: {...}` SSE line was split across two chunks,
   the JSON.parse in the following try/catch would throw and the
   whole token for that line was silently discarded — the catch
   block was empty, so this failure mode left no trace in the logs.

Both are now fixed: decode with { stream: true } so partial UTF-8
sequences are held over instead of corrupted, and lineBuffer carries
over any trailing incomplete line to be prefixed onto the next chunk
before splitting/parsing. Reproducible by asking a question in a
non-ASCII language (e.g. Russian) and getting back responses with
missing letters or whole word fragments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant