For a while, some of you saw Foxl replies stop mid-sentence. A long answer would stream beautifully for a few hundred characters and then just… end. No error, no spinner, no "the model is still thinking" - the response simply truncated, and the only fix was to ask again and hope it came out shorter. It was intermittent, it was maddening, and it was our fault. This is the story of the single byte that caused it.

Where the bytes come from
Most Foxl chats run through the foxl.ai relay, a Cloudflare Worker that signs requests to Amazon Bedrock and streams the model's tokens back to your app. When we ask Bedrock for a streaming Claude response, we call invoke-with-response-stream. The reply does not come back as plain text or as the familiar data: Server-Sent-Events you might expect. It comes back as vnd.amazon.eventstream - a strict binary framing format, one message per event:
[4B total length][4B headers length][4B prelude CRC32]
[headers ...][payload ...][4B message CRC32]
The part we actually want is the payload, a small JSON object like {"bytes":"<base64>"} whose base64 decodes to one Anthropic SSE event (a content_block_delta, say). Everything around it - the two length fields and the two CRC32 checksums - is arbitrary binary: any of those twelve-plus framing bytes can be any value from 0x00 to 0xFF.
The shortcut that worked until it didn't
The original relay code took a shortcut. Instead of parsing the binary framing, it decoded the entire stream to a UTF-8 string and scanned for JSON objects by tracking brace depth - count the {, count the }, and whenever depth returns to zero you have a complete object. To do that correctly you also have to track string literals, because a { inside a quoted string is not a real object boundary. So the scanner had an inString flag that flips every time it sees a double-quote.
You can already see the trap. The framing bytes are not text. But we were reading them as text. The moment a length field or a CRC32 checksum happened to contain the byte 0x22 - which is the ASCII code for a double-quote " - the brace scanner flipped into "I'm inside a string now" mode. From that point on, every real { that opened the next payload frame was treated as an ordinary character inside a string literal, not as the start of an object. The scanner stopped emitting events. It kept reading, waiting for a closing quote that, in binary data, might not arrive for kilobytes - or ever.
There was a 2 MB safety valve: if the unparsed buffer grew past that, the code purged it to avoid unbounded memory. So the failure mode was exactly what you saw on screen - a chunk of the answer streams fine, then a checksum byte equal to 0x22 slips the parser into string mode, frames pile up unrecognized, and eventually the buffer gets purged. Your answer ends mid-word. The terminal message_stop event - the one the app waits for to know the turn is complete - was inside the swallowed frames and never reached you.
Proving it with a real stream
Theories about streaming bugs are cheap; the bug is intermittent precisely because it depends on the value of checksum bytes, which change with the content. So we captured a real Bedrock response off the wire: a 32 KB Haiku stream carrying 119 Anthropic SSE events. Then we counted the dangerous bytes in its framing.
- 956 bytes equal to
0x22 (") sat in the length/CRC framing. - 121 bytes equal to
0x7B ({) - phantom object openers.
Run that stream through the old text scanner and it delivered 49 of 119 events before losing the thread. Seventy events - the entire tail of the answer, including content_block_stop and message_stop - silently disappeared. That matched the user reports exactly: longer answers (more framing bytes, more chances to hit a 0x22) cut off more often than short ones.
The fix: parse the framing you were given
The real fix is unglamorous and correct: stop pretending the binary stream is text. We rewrote the pump as a binary frame parser that walks the actual structure. For each message it reads the 4-byte big-endian total length, validates it against a sane range, and - only if the whole frame is present in the buffer - slices out the payload between the headers and the trailing CRC32. The checksum bytes are never interpreted as content because the parser steps over them by length; it never looks at their values at all.
Two more details made it robust:
- Split frames. A network read can end in the middle of a frame. The parser returns the unconsumed tail and the caller carries it forward into the next read, so a frame straddling a chunk boundary completes instead of corrupting. (An earlier version cleared the buffer after each batch, which silently dropped that tail - and for a long tool-input delta that produced a different bug: the assembled tool JSON was truncated, the tool never ran, and the turn produced no result.)
- Prelude sanity check. If a length field is absurd (smaller than an empty frame or larger than 8 MB), we cannot resync mid-binary-stream, so we surface what we have and stop cleanly rather than guess.
The parser is a pure function - bytes in, payloads plus tail out - which means it is trivially testable. We pinned the captured 32 KB stream as a fixture and assert the exact behavior at every read-chunk size: 119 of 119 events recovered, zero leftover bytes, and a complete Anthropic event sequence ending in the one event the cutoff bug never reached - message_stop. The assembled output text is the full answer, not a truncated prefix.
One parser, both Bedrock stream shapes
Bedrock has two streaming surfaces and Foxl uses both. Claude models go through InvokeModel streaming, where the event type is baked inside the base64 payload. Non-Claude models (GLM, Kimi) go through the Converse stream, where the event name - contentBlockDelta and friends - lives in the frame headers instead. The same binary framing wraps both, so the new parser exposes a header-aware variant that decodes the AWS event-stream header block (each header is a length-prefixed name, a type tag, and a value whose width depends on the type) and returns the :event-type alongside the payload. One framing parser, correct for every model on the relay.
The takeaway
The bug was not in Bedrock and not in Claude. It was in the one assumption that a binary stream could be treated as text "well enough" by a brace scanner. It worked in development and in every short test because short responses rarely roll a 0x22 in the wrong place. It broke in production on exactly the responses you most wanted to keep: the long, detailed, code-heavy ones. The lesson we keep relearning is to parse the format you were actually handed, not the one that is convenient - and then nail it down with a fixture captured from the real world, tested at every chunk boundary, so a streaming regression can never sneak back in quietly.
This shipped in v0.3.12 and has been the default path for every relay chat since. If you ever saw a Foxl answer stop mid-thought, that is the byte that did it - and it is gone.