#3

Multi-line JSON WebSocket messages break SSE framing

Openrachel-paceopened 8mo ago

Bug

The onMessage handler in createWebSocketFetch wraps each WebSocket message as a single SSE data: line:

function onMessage(data) {
  const text = data.toString();
  controller.enqueue(encoder.encode(`data: ${text}\n\n`));
}

When OpenAI sends pretty-printed (multi-line) JSON over WebSocket, the newlines inside the JSON break SSE framing. The downstream EventSourceParserStream splits on newlines, so a message like:

{
  "type": "response.created",
  "response": { ... }
}

becomes:

data: {
  "type": "response.created",
  "response": { ... }
}

The SSE parser sees data: { as the only data line, dispatches an event with data {, and JSON parsing fails:

[AI_JSONParseError]: JSON parsing failed: Text: {.
Expected property name or '}' in JSON at position 1

Reproduction

  • Use createWebSocketFetch() with @ai-sdk/openai and streamText
  • Include a provider-defined tool like openai.tools.shell() (forces Responses API path)
  • Use gpt-5-mini, which sends multi-line JSON in some WebSocket responses
  • gpt-5.4 sends compact single-line JSON so it doesn't trigger the issue

Suggested fix

Use multi-line SSE data format so newlines in the payload are preserved:

function onMessage(data) {
  const text = data.toString();
  const lines = text.split('\n').map(line => `data: ${line}`).join('\n');
  controller.enqueue(encoder.encode(`${lines}\n\n`));
}

This follows the SSE spec — multiple data: lines are concatenated with \n by the parser, reconstructing the original multi-line content.

Activity

1
balassit3mo ago

Confirming this in production, and flagging that the fix appears to be merged but not published.

We hit the identical error on the Responses API over WS:

[AI_JSONParseError]: JSON parsing failed: Text: {.
Expected property name or '}' in JSON at position 1 (line 1 column 2)

It reaches the user as a 500, because the parse failure surfaces inside streamText with no partial content, so our error-fallback logic can't distinguish it from a hard stream failure and rethrows.

One extra data point on triggering conditions: for us it is bursty rather than constant — 21 occurrences in one hour, 9 in another, then none for two days. That's consistent with the report that only some models/events pretty-print, rather than it being deterministic per model.

The reason I'm commenting: the fix in bb59a5e (#2) looks correct, but it doesn't seem to have made it to npm.

$ npm view @vercel/ai-sdk-openai-websocket-fetch time --json
  created   2026-04-11T01:01:42.224Z
  1.0.0     2026-04-11T01:01:42.482Z      <- only version ever published

bb59a5e is dated 2026-05-15, about a month after that publish. Unpacking the current tarball confirms the published build still has the single-line framing:

$ npm pack @vercel/ai-sdk-openai-websocket-fetch && tar xzf *.tgz
$ sed -n '65,70p' package/dist/index.js
        function onMessage(data) {
          const text = data.toString();
          controller.enqueue(encoder.encode(`data: ${text}

`));

No split(/\r?\n/) and no response.failed / response.incomplete handling in the shipped artifact.

So anyone installing from npm today still gets the bug, and there's no version to upgrade to — we've had to vendor the module locally to pick up the fix. A patch release (1.0.1) would let us drop that.

Happy to test a release candidate against our workload if useful.