openclaw - 💡(How to fix) Fix HTTP SSE (/v1/chat/completions) drops leading '<' from streamed content and closes connection before final chunk [1 pull requests]

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…

When consuming the /v1/chat/completions SSE endpoint, two bugs occur that do not affect the WebSocket (control UI) path:

  1. Leading < is occasionally dropped from streamed content
  2. data: [DONE] is sent before the final content chunk arrives, and the HTTP connection is closed immediately after — the final chunk is permanently lost

Both issues are reproducible when the assistant's response contains custom XML-style tags (e.g. <xiaohai-banli>...</xiaohai-banli>). The WebSocket path produces correct output every time.


Root Cause

Root cause: splitTrailingBlockTagFragment() in selection-CEUEYS7j.js

Fix Action

Fixed

Code Example

function isPotentialTrailingBlockTagFragment(fragment) {
    // ...
    const candidate = normalized.slice(1).replace(/^\//, "");
    if (!candidate) return true;   // ← bare "<" is always held back
    return STREAM_STRIPPED_BLOCK_TAG_NAMES.some((name) => name.startsWith(candidate));
}

---

// Before
if (!candidate) return true;

// After — bare "<" alone cannot be identified as a block-tag prefix
// without at least one following character; do not hold it back
if (!candidate) return false;

---

// selection-CEUEYS7j.js
if (evtType === "text_end") {
    Promise.resolve().then(() => ctx.flushBlockReplyBuffer({ final: true }));
    //                  ↑ microtask — scheduled but not yet executed
}

---

// Before
const maybeFinalize = () => {
    // ...
    writeDone(res);
    res.end();
};

// After
const maybeFinalize = () => {
    // ...
    Promise.resolve().then(() => {
        if (!closed) return;
        writeDone(res);
        res.end();
    });
};
RAW_BUFFERClick to expand / collapse

Environment

  • OpenClaw version: 2026.4.30
  • Transport: HTTP SSE (/v1/chat/completions)
  • Comparison: WebSocket path works correctly

Description

When consuming the /v1/chat/completions SSE endpoint, two bugs occur that do not affect the WebSocket (control UI) path:

  1. Leading < is occasionally dropped from streamed content
  2. data: [DONE] is sent before the final content chunk arrives, and the HTTP connection is closed immediately after — the final chunk is permanently lost

Both issues are reproducible when the assistant's response contains custom XML-style tags (e.g. <xiaohai-banli>...</xiaohai-banli>). The WebSocket path produces correct output every time.


Bug 1: Leading < dropped from delta

Root cause: splitTrailingBlockTagFragment() in selection-CEUEYS7j.js

function isPotentialTrailingBlockTagFragment(fragment) {
    // ...
    const candidate = normalized.slice(1).replace(/^\//, "");
    if (!candidate) return true;   // ← bare "<" is always held back
    return STREAM_STRIPPED_BLOCK_TAG_NAMES.some((name) => name.startsWith(candidate));
}

When a streaming chunk ends with a bare < (the LLM emitted < and xiaohai-banli> as separate tokens), the function holds it as pendingTagFragment and strips it from the emitted text. On the next chunk the < is internally re-prepended, but resolveAssistantStreamDeltaText() in the SSE handler reads evt.data.delta (incremental). The delta computation treats the < as already accounted for, so it is never written to the HTTP response.

The WebSocket path reads evt.data.text (full cumulative string) via resolveMergedAssistantText(), so the < is always present regardless of how deltas were chunked.

Suggested fix (selection-CEUEYS7j.js):

// Before
if (!candidate) return true;

// After — bare "<" alone cannot be identified as a block-tag prefix
// without at least one following character; do not hold it back
if (!candidate) return false;

Bug 2: HTTP connection closed before final chunk is written

Root cause: maybeFinalize() in openai-http-4AOdFadL.js is a synchronous function. When lifecycle:end fires, it immediately calls writeDone(res); res.end(). However, the final content flush in selection-CEUEYS7j.js is deferred via a microtask:

// selection-CEUEYS7j.js
if (evtType === "text_end") {
    Promise.resolve().then(() => ctx.flushBlockReplyBuffer({ final: true }));
    //                  ↑ microtask — scheduled but not yet executed
}

Execution order:

  1. text_end → microtask queued (contains final closing tag, e.g. </xiaohai-banli>)
  2. lifecycle:endsynchronouswriteDone(res); res.end()connection closed
  3. Microtask fires → connection already closed → final chunk permanently lost

The WebSocket path calls flushBufferedChatDeltaIfNeeded() synchronously inside emitChatFinal() before broadcasting state: "final", so it always delivers the complete text.

Suggested fix (openai-http-4AOdFadL.js): defer writeDone/res.end() by one microtask tick so any pending Promise.resolve().then(...) callbacks can flush first:

// Before
const maybeFinalize = () => {
    // ...
    writeDone(res);
    res.end();
};

// After
const maybeFinalize = () => {
    // ...
    Promise.resolve().then(() => {
        if (!closed) return;
        writeDone(res);
        res.end();
    });
};

Reproduction

  1. Configure a skill whose response contains a custom XML-style tag (e.g. <xiaohai-banli>data: {...}</xiaohai-banli>)
  2. Send a request via HTTP SSE (/v1/chat/completions)
  3. Observe: the < at the start of the tag is missing ~30–50% of the time (depends on token boundary), and data: [DONE] arrives before the closing tag

Connecting the same session via WebSocket (control UI) produces correct output consistently.


Impact

Any downstream consumer that parses custom XML-style tags from the HTTP SSE stream will silently receive malformed or truncated output. The WebSocket path is unaffected.

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…

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 HTTP SSE (/v1/chat/completions) drops leading '<' from streamed content and closes connection before final chunk [1 pull requests]