openclaw - 💡(How to fix) Fix openai-node streaming: event-only SSE flush causes JSON.parse('') crash (Unexpected end of JSON input) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#52802Fetched 2026-04-08 01:19:12
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

When consuming an SSE stream, if an SSE “message” is flushed with an event: field but no data: lines (i.e. event: … followed by a blank line), the OpenAI Node SDK attempts JSON.parse on an empty string and throws:

  • Unexpected end of JSON input

This shows up downstream in OpenClaw as intermittent job failures (e.g. startup-sync-guard, operator-digest, acp-*) when an OpenAI-compatible proxy/gateway incorrectly splits event: and data: across SSE message boundaries.

Root Cause

In openai/core/streaming.*:

  • SSEDecoder.decode() flushes an SSE object on a blank line whenever either event is set or any data lines exist.
  • Stream.fromSSEResponse() then unconditionally does JSON.parse(sse.data).

So an event: line followed by a blank line produces an event-only message with data: "", which crashes JSON parsing.

Fix Action

Fix / Workaround

Temporary local hotfix that unblocked OpenClaw jobs

In class SSEDecoder { decode(line) { ... } }, inside the if (!line) { ... } branch:

Code Example

import { Stream } from "openai/streaming";

const encoder = new TextEncoder();

// Simulate a proxy/gateway producing an event-only SSE message.
// Note: This is two SSE messages:
// 1) "event: message" with NO data (blank line flush)
// 2) a data-only message that would be parseable if we didn’t crash on (1)
const sseWire = [
  "event: message\n",
  "\n",
  "data: {\"ok\": true}\n",
  "\n",
].join("");

const body = new ReadableStream({
  start(controller) {
    controller.enqueue(encoder.encode(sseWire));
    controller.close();
  },
});

const response = new Response(body, {
  headers: { "content-type": "text/event-stream" },
});

const abort = new AbortController();

for await (const item of Stream.fromSSEResponse(response, abort)) {
  console.log("item:", item);
}

---

node repro.mjs

---

// Treat event-only chunks as incomplete and wait for `data:`.
if (this.event && !this.data.length) {
  return null;
}
RAW_BUFFERClick to expand / collapse

Summary

When consuming an SSE stream, if an SSE “message” is flushed with an event: field but no data: lines (i.e. event: … followed by a blank line), the OpenAI Node SDK attempts JSON.parse on an empty string and throws:

  • Unexpected end of JSON input

This shows up downstream in OpenClaw as intermittent job failures (e.g. startup-sync-guard, operator-digest, acp-*) when an OpenAI-compatible proxy/gateway incorrectly splits event: and data: across SSE message boundaries.

Environment

  • OpenClaw: 2026.3.13 (61d171a)
  • OpenAI Node SDK: 6.26.0 (vendored in OpenClaw install)
  • Node.js: v25.8.1
  • Platform: Linux x86_64

Minimal reproduction (no API keys; pure in-memory Response)

Save as repro.mjs:

import { Stream } from "openai/streaming";

const encoder = new TextEncoder();

// Simulate a proxy/gateway producing an event-only SSE message.
// Note: This is two SSE messages:
// 1) "event: message" with NO data (blank line flush)
// 2) a data-only message that would be parseable if we didn’t crash on (1)
const sseWire = [
  "event: message\n",
  "\n",
  "data: {\"ok\": true}\n",
  "\n",
].join("");

const body = new ReadableStream({
  start(controller) {
    controller.enqueue(encoder.encode(sseWire));
    controller.close();
  },
});

const response = new Response(body, {
  headers: { "content-type": "text/event-stream" },
});

const abort = new AbortController();

for await (const item of Stream.fromSSEResponse(response, abort)) {
  console.log("item:", item);
}

Run:

node repro.mjs

Expected behavior

  • The SDK should ignore SSE messages with empty/whitespace-only data (or at least not attempt JSON.parse on ""), and continue consuming subsequent valid SSE messages.

Actual behavior

  • The SDK throws Unexpected end of JSON input because it executes JSON.parse(sse.data) when sse.data === "".

Root cause

In openai/core/streaming.*:

  • SSEDecoder.decode() flushes an SSE object on a blank line whenever either event is set or any data lines exist.
  • Stream.fromSSEResponse() then unconditionally does JSON.parse(sse.data).

So an event: line followed by a blank line produces an event-only message with data: "", which crashes JSON parsing.

Temporary local hotfix that unblocked OpenClaw jobs

In class SSEDecoder { decode(line) { ... } }, inside the if (!line) { ... } branch:

// Treat event-only chunks as incomplete and wait for `data:`.
if (this.event && !this.data.length) {
  return null;
}

Suggested official fix direction

One of:

  1. Preferred (minimal): In Stream.fromSSEResponse, skip JSON.parse for empty/whitespace sse.data (treat as keepalive / malformed event).
  2. Alternative: In SSEDecoder.decode(), do not flush a message when there are no data: lines.

Suggested tests

Add unit tests that build an in-memory Response (like the repro) and assert:

  1. No throw on event: message\n\n (event-only flush); yields no items.
  2. Event-only flush followed by a valid data: message: no throw; yields the later parsed object.
  3. CRLF delimiter variants (\r\n\r\n).

extent analysis

Fix Plan

To fix the issue, we can modify the Stream.fromSSEResponse function to skip JSON.parse for empty or whitespace sse.data. Here are the steps:

  • Open the openai/streaming.js file and locate the Stream.fromSSEResponse function.
  • Add a conditional check to skip JSON.parse when sse.data is empty or contains only whitespace:
for await (const sse of SSEDecoder.decodeStream(response.body)) {
  if (sse.data && sse.data.trim() !== '') {
    yield JSON.parse(sse.data);
  } else {
    // Ignore event-only messages or messages with empty data
    continue;
  }
}

Alternatively, you can modify the SSEDecoder.decode function to not flush a message when there are no data: lines:

if (!line) {
  if (this.event && !this.data.length) {
    return null;
  }
  // ...
}

Verification

To verify the fix, run the repro.mjs script again and check that it no longer throws an error. You can also add unit tests to ensure that the fix works correctly for different scenarios.

Extra Tips

  • Make sure to test the fix with different types of SSE messages, including event-only messages, messages with empty data, and messages with valid data.
  • Consider adding a check for sse.data being null or undefined to handle cases where the data: line is missing.
  • If you choose to modify the SSEDecoder.decode function, make sure to test it thoroughly to ensure that it does not introduce any new issues.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

  • The SDK should ignore SSE messages with empty/whitespace-only data (or at least not attempt JSON.parse on ""), and continue consuming subsequent valid SSE messages.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix openai-node streaming: event-only SSE flush causes JSON.parse('') crash (Unexpected end of JSON input) [1 participants]