[Bug] SSE stream data corruption: tool call arguments truncated, Chinese text lost, JSON concatenation errors
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 19.9k
- Forks
- 2.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 30
Description
Description
When using OpenAI-compatible providers via the OpenRouter SSE stream, three forms of data corruption occur:
1. JSON concatenation
The proxy/upstream removes the \n\n event separator between two SSE data: lines, producing concatenated JSON like {"id":"a"}{"id":"b"...} or {"id":"a"}data: {"id":"b"...}. serde_json::from_str rejects this with Extra data and drops the entire chunk.
2. Cross-event JSON truncation
The proxy splits a JSON object across two SSE events:
- Event 1:
data: {"choices":[{"delta":{"content":"Hello " - Event 2:
data: world"}}]}
The first event's brace depth never reaches zero, so the chunk is discarded.
3. UTF-8 boundary corruption
When a multi-byte UTF-8 character (e.g. Chinese 读 = 3 bytes) is split across TCP chunks, std::str::from_utf8() fails on the individual chunk, and the entire chunk is silently dropped. This manifests as missing Chinese characters or \u{FFFD} replacement characters.
Symptoms
- Tool calls fail with
Invalid tool call: arguments must be a JSON object, got null - Tool call arguments truncated mid-string
- Chinese text in table columns missing or corrupted
- Data written via
writetool has missing content - Frequent
OpenRouter SSE JSON parse failedwarnings in logs
Root Causes
-
String buffer with
from_utf8per chunk: UsingStringas SSE buffer requires callingfrom_utf8on each incoming TCP chunk. When a multi-byte character is split across chunks,from_utf8fails and the entire chunk is lost. -
serde_json::from_strrejects concatenated JSON: Multiple JSON values in one string causeExtra dataerror; the entire blob is discarded instead of extracting individual values. -
No partial JSON reassembly: When a JSON object is truncated mid-stream (brace never closes), there is no mechanism to save the partial text and combine it with the next event.
Impact
- Data loss: Tool call arguments are silently truncated
- Failed execution:
arguments=nullcauses tool calls to be rejected - Display corruption: Table columns vanish, Chinese characters replaced with
� - Frequency: Hundreds of parse errors per session with affected providers
Proposed Fix
The fix addresses all three root causes:
a) Raw byte buffer instead of String
Change the SSE buffer from String to Vec<u8>. Accumulate raw bytes and only decode to String after a complete SSE event boundary (\n\n or \r\n\r\n) is found, ensuring UTF-8 sequences are always complete.
b) Brace-depth JSON splitting
Replace serde_json::from_str with a brace-depth matcher that scans {/} nesting. This correctly handles both concatenated JSONs ({"a":1}{"b":2}) and embedded data: prefix ({"a":1}data: {"b":2}).
c) Partial JSON reassembly
When brace-depth never reaches zero (cross-event truncation), save the incomplete JSON text and prepend it to the next event's data before parsing.
d) Stream-end buffer flush
When the SSE stream closes without a trailing \n\n, force-close the buffer and process remaining data.
Log evidence (before vs after fix)
| Metric | Before (8h session) | After (12h session) |
|---|---|---|
| SSE JSON parse failures | 719 | 4 (edge cases) |
| Invalid tool calls | 74 | 1 |
Environment
- jcode version: v0.58.x
- Provider: OpenAI-compatible (DeepSeek, OpenCode Go, etc.)
- Transport: SSE over HTTPS
- Models: deepseek-v4-flash, glm-5
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Locate the SSE buffering and JSON parsing entry point, then reproduce the listed concatenated-JSON, cross-event truncation, UTF-8 boundary, and stream-end cases. Done means complete SSE events preserve multibyte text, concatenated and partial JSON values are processed, trailing data is flushed, and the reported parse failures and invalid tool calls are reduced.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100