fix(anthropic): bound streaming 200 success-body SSE reads at 16 MiB (provider path)#96723
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 27, 2026, 1:00 PM ET / 17:00 UTC. Summary PR surface: Source +20, Tests +37. Total +57 across 2 files. Reproducibility: yes. from source: current main's Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this provider-path guard if maintainers accept the 16 MiB boundary; otherwise tune the cap and matching regression assertion before merge. Do we have a high-confidence way to reproduce the issue? Yes from source: current main's Is this the best way to solve the issue? Yes, with a compatibility caveat: reusing the existing core guard is the narrowest maintainable fix for this parser. A new SDK export or config knob would be broader than this bugfix unless maintainers reject or tune the fixed threshold. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2720ac06b7c8. Label changesLabel justifications:
Evidence reviewedPR surface: Source +20, Tests +37. Total +57 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
dd7819a to
3407ee1
Compare
|
@clawsweeper re-review Rebased to latest main (fixes CI). Upgraded proof to 🦞-grade: added BEFORE (unbounded) comparison showing 64.0 MiB OOM vector confirmed (4× the cap), negative control with bytes-sent quantification, sibling PR references (#96772, #96782, #96777), and AI disclosure. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
… at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body).
… at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body).
… at 16 MiB (#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in #96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR #96666, after PR #3b (#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]>
… at 16 MiB (openclaw#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]>
… at 16 MiB (openclaw#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]>
|
@clawsweeper re-review 🦞 upgrades applied (PR body only):
No code changes. |
…(provider path) Apply createSseByteGuard to src/llm/providers/anthropic.ts (legacy provider iterateSseMessages) so the Anthropic Messages provider SSE parser cannot be exhausted by a hostile or malfunctioning Anthropic-compatible endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/anthropic.ts: wrap body.getReader() in createSseByteGuard before the existing iterateSseMessages loop. The function is also re-exported as iterateSseMessagesForTest for direct test access. - Inline test added to anthropic.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the second of the planned per-surface rescue series for the previously closed PR openclaw#96632, after openclaw#96701. No SDK surface change. No repro script committed (proof in this body). The companion error-body path is already bounded (readAnthropicMessages ErrorBodySnippet, 8 KiB + 10s idle timeout, openclaw#95108); this PR closes the legacy-provider success-body gap.
3407ee1 to
7c3dea3
Compare
|
@clawsweeper re-review Rebased onto current main. The Diff: 2 files, +59/-2 (no more |
|
🦞🧹 I asked ClawSweeper to review this item again. |
… at 16 MiB (openclaw#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]>
… at 16 MiB (openclaw#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]>
… at 16 MiB (openclaw#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]>
… at 16 MiB (openclaw#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]>
|
Closing as duplicate of #96701 / superseded by #96762. Decision: This PR and #96701 both bound Anthropic streaming SSE at 16 MiB via Why: 14 days open with no maintainer movement; the broader 16 MiB body-bound initiative (#96680 cluster) was closed out on 2026-06-29 in a maintainer batch that did not select this branch. Supported alternative: A one-line follow-up PR that imports Evidence that would change the decision: a maintainer explicitly asks for this exact branch to land (vs. the follow-up), or a new finding that the helper exported by #96762 does not actually fit the Anthropic provider path. 🤖 Generated with Claude Code |
… at 16 MiB (openclaw#96762) * fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB Apply createSseByteGuard to src/llm/providers/openai-chatgpt-responses.ts (parseSSE) so the ChatGPT Responses SSE parser cannot be exhausted by a hostile or malfunctioning endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/llm/providers/openai-chatgpt-responses.ts: wrap body.getReader() in createSseByteGuard before the existing parseSSE loop. The function is also re-exported as parseSSEForTest for direct test access. - Inline test added to openai-chatgpt-responses.test.ts: hostile 1 MiB pull stream, asserts canonical overflow message, cancel(reason) was an Error instance, pullCount bounded to 17-20. Reuses the createSseByteGuard helper from src/agents/streaming-byte-guard.ts (introduced in openclaw#96701 for the anthropic-transport-stream path; same helper, same 16 MiB cap, same overflow-error shape). This PR is the third of the planned per-surface rescue series for the previously closed PR openclaw#96666, after PR #3b (openclaw#96723). No SDK surface change. No repro script committed (proof in this body). * fix(openai): bound ChatGPT error body reads * fix(openai): bound chatgpt error parsing --------- Co-authored-by: Vincent Koc <[email protected]> (cherry picked from commit 5880e0a)
What Problem This Solves
src/llm/providers/anthropic.ts(iterateSseMessages, the legacy provider SSE parser) accumulates an unbounded SSE byte stream while walking the Anthropic Messages streaming response. A hostile or malfunctioning Anthropic-compatible endpoint can return aContent-Length-less SSE body that streams forever, exhausting process memory on the legacy code path that runs alongside the transport-stream path (#96701).This is the streaming-body analogue of the bounded-read sweep Alix-007 finished for non-streaming
response.json()/response.text(). Same 16 MiB cap, same helper-driven design, on the legacy provider path that #96701 does not cover.Changes
Reuses the existing
createSseByteGuardhelper (already in main) — no new abstraction in this PR.src/llm/providers/anthropic.ts:347-362—iterateSseMessageswrapsbody.getReader()increateSseByteGuardwithANTHROPIC_MESSAGES_SUCCESS_BODY_MAX_BYTES = 16 * 1024 * 1024.src/llm/providers/anthropic.test.ts— 1 inline test (38 LoC) through productioniterateSseMessageswrapper with oversized stream + negative control + happy path.Symmetric counterpart to #96701 (anthropic-transport-stream). Aligned with the bounded-read campaign pattern.
Design Rationale
Why reuse
createSseByteGuardinstead of a new helper? The provider-path SSE parser follows the same chunk-by-chunkreader.read()→ buffer → parse loop structure as anthropic-transport-stream. Using the same guard avoids duplicating the byte-accounting, overflow-check, and cancellation logic. The guard'sSseByteGuardinterface (read/cancel/totalBytes/overflowed/cancelled) was designed for cross-parser reuse from the start.Why 16 MiB cap? Matches the existing non-streaming 16 MiB cap from
readProviderJsonResponse(src/agents/provider-http-errors.ts) and the sibling anthropic-transport-stream PR (#96701). The Anthropic Messages API produces compact streaming responses — typical SSE events are a few KiB, so the 16 MiB cap would only fire on malfunctioning or hostile endpoints.Why a separate call site, not a shared constant? Each parser (anthropic transport, proxy, provider) evolves independently — the provider-path cap may later be tuned differently if legacy provider patterns emerge. Named constants at each call site keep the override path explicit while the value stays consistent (all 16 MiB).
Real behavior proof
node:httpserver bound to127.0.0.1returning aContent-Length-less 64 MiB body (4× the cap) chunk-by-chunk, plus a negative-control 1 MiB body, plus a happy-path body. Drives the productioniterateSseMessagesover a real TCP socket. Node v22.22.0.node --import tsx _proof_anthropic_provider_stream.mts(proof script kept local-only, not committed).createSseByteGuardcancelled the upstreamReadableStreamDefaultReaderat ~16 MiB. Server-sidebytesSentobserved at ~19 MiB — bounded, not drained.Out of scope (separate follow-ups)
src/agents/runtime/proxy.ts(streamProxy, internal proxy SSE parser) — fix(runtime/proxy): bound streaming success-body SSE reads at 16 MiB #96768.extensions/google/transport-stream.tsandextensions/ollama/src/{setup,stream}.ts— need the helper promoted to a plugin-SDK subpath first (separate foundation PR).Risk checklist
createSseByteGuardto underlying reader; verified by inline test assertion.Diff stats