fix(openai-chatgpt-responses): bound streaming success-body SSE reads at 16 MiB#96762
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 26, 2026, 8:32 PM ET / 00:32 UTC. Summary PR surface: Source +152, Tests +92. Total +244 across 3 files. Reproducibility: yes. as a source-reproducible path: current main and v2026.6.10 read ChatGPT Responses SSE chunks and error bodies without byte caps. I did not run tests in this read-only pass; the PR body proves the success-stream path with local TCP output, but not the newly added error-body cap. 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:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the narrow bounded-read fix after current-head proof covers both success-stream and error-body paths, maintainers accept or tune the caps, and the shared helper sequencing is clean. Do we have a high-confidence way to reproduce the issue? Yes, as a source-reproducible path: current main and v2026.6.10 read ChatGPT Responses SSE chunks and error bodies without byte caps. I did not run tests in this read-only pass; the PR body proves the success-stream path with local TCP output, but not the newly added error-body cap. Is this the best way to solve the issue? Yes, if maintainers accept the limits. Guarding the reader preserves streaming and avoids public SDK surface, while the existing full-body bounded reader would buffer before parsing; the safer merge path is updated current-head proof plus explicit cap acceptance. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 4d9cd7d22755. Label changesLabel justifications:
Evidence reviewedPR surface: Source +152, Tests +92. Total +244 across 3 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
|
41ecb31 to
81a0f2a
Compare
… 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).
962908b to
5a99681
Compare
|
Maintainer proof for the synced/fixed head:
Proceeding with the repo merge wrapper now. |
|
Merged via squash.
|
|
Maintainer proof after refresh to current main:
|
|
Maintainer proof after refresh to current main:
Best-fix verdict: good to land. The provider now caps both high-I/O response-body paths in this flow instead of only the happy-path stream. |
|
Merged after maintainer proof and fix. What landed:
Proof:
Landed on main as |
… 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]>
Apply createSseByteGuard to src/agents/runtime/proxy.ts (streamProxy) so the runtime proxy SSE parser cannot be exhausted by a hostile or malfunctioning proxy endpoint that streams an unbounded SSE body. The 16 MiB cap matches the non-streaming readProviderJsonResponse cap. What changed: - src/agents/runtime/proxy.ts: wrap response.body.getReader() in createSseByteGuard before the existing streamProxy SSE read loop. The abort handler and finally block use guard.cancel() instead of reader.cancel(). - Inline test added to proxy.test.ts: hostile 1 MiB pull stream through streamProxy, asserts errorMessage matches overflow pattern, 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 final per-surface rescue from the previously closed PR openclaw#96666, after PR #3c (openclaw#96762). No SDK surface change. No repro script committed (proof in this body).
… 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]>
… 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/openai-chatgpt-responses.ts(parseSSE, the ChatGPT Responses SSE parser) accumulates an unbounded SSE byte stream into a string buffer while waiting for\n\nframe delimiters. A hostile or malfunctioning ChatGPT Responses endpoint (including a MITM-able proxy or a hijacked self-hosted provider) can return aContent-Length-less SSE body that streams forever, exhausting process memory on a code path that runs for every SSE-transport ChatGPT Responses streaming call.This is the streaming-body analogue of the bounded-read sweep Alix-007 finished for non-streaming
response.json(). Same 16 MiB cap, same helper-driven design, but on the chunk-by-chunk SSE path that the non-streaming sweep did not cover.Changes
src/agents/streaming-byte-guard.ts(NEW, 87 lines, same file from fix(anthropic): bound streaming 200 success-body SSE reads at 16 MiB #96701) —createSseByteGuardhelper reused from the anthropic-transport-stream PR. Same minimal core-internal helper.src/llm/providers/openai-chatgpt-responses.ts:724—parseSSEwrapsbody.getReader()increateSseByteGuardwithOPENAI_CHATGPT_RESPONSES_SUCCESS_BODY_MAX_BYTES = 16 * 1024 * 1024. The existing SSE frame parsing loop and cleanup are unchanged. The function is also re-exported asparseSSEForTestfor direct test access.src/llm/providers/openai-chatgpt-responses.test.ts— inline test: hostile 1 MiB pull stream, asserts canonical overflow message,cancel(reason)was anErrorinstance,pullCountbounded to 17-20.Real behavior proof
parseSSESSE read; after the fix the read is capped at 16 MiB and the stream is cancelled on overflow.node:httpserver bound to127.0.0.1returning aContent-Length-less 64 MiB body (4× the cap) chunk-by-chunk with backpressure, plus a negative-control 1 MiB body (1135 SSE events), plus a happy-path body (2 events). Drives the productionparseSSEForTestover a real TCP socket. Node v22.22.0.node --import tsx _proof_openai_responses_stream.mts(proof script kept local-only, not committed, matching Alix-007 fix(github-copilot): bound usage response reads #96607 pattern).createSseByteGuardcancelled the upstreamReadableStreamDefaultReaderat ~16 MiB (well under 64 MiB). Server-sidesocket.bytesWrittenobserved at 19,923,323 — bounded, not drained.cancel(reason)received anErrorinstance. Negative control confirms the cap is the cause of overflow, not body structure.Out of scope (separate PRs, sequenced)
src/agents/runtime/proxy.ts(streamProxy, internal proxy SSE parser) — same bounded-read pattern, separate PR (planned as PR #3d).extensions/google/transport-stream.tsandextensions/ollama/src/{setup,stream}.ts— need the helper promoted to a plugin-SDK subpath first.Risk
readProviderJsonResponsefor non-streaming ChatGPT Responses success bodies. The behavior for normal-sized responses is unchanged — only the unbounded case now throws a clear overflow error instead of buffering without limit.createSseByteGuard.cancel(reason)propagates the overflow error to the underlying reader; verified byexpect(cancelReason).toBeInstanceOf(Error)in the test.src/agents/streaming-byte-guard.ts); nopackage.jsonexport, noscripts/lib/plugin-sdk-entrypoints.jsonentry, no API-baseline regen needed.createSseByteGuardsignature, same 16 MiB cap, same overflow-error shape. No new abstraction introduced.Diff stats
Code change (excluding the inline test file): 106 LoC (87 helper + 19 source). Test additions: 43 LoC inline in existing describe block.