fix(agents): bound body-less MCP HTTP text responses#98143
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 5, 2026, 7:42 PM ET / 23:42 UTC. Summary PR surface: Source -2, Tests +27, Docs +1. Total +26 across 3 files. Reproducibility: yes. source-reproducible: current main reaches Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. 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 the fail-closed MCP transport fix after exact-head gates, keeping raw OAuth body parsing only for standards-compliant streamed responses. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible: current main reaches Is this the best way to solve the issue? Yes, this is the best available fix shape for the availability risk: the owner boundary is the MCP fetch Response normalizer, and the alternative Content-Length/text-cap designs still call an unbounded AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2ded26a5d6be. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source -2, Tests +27, Docs +1. Total +26 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
Review history (5 earlier review cycles)
|
|
@clawsweeper re-review Addressed the previous compatibility/availability finding by making the bodyless foreign-response policy explicit. Policy now:
This means the no-length allocation path identified in the review no longer calls Local proof:
{"declared":{"status":400,"body":null},"unbounded":{"status":400,"body":null},"textCalls":0,"declaredTextCalls":0} |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review Updated the PR body with the latest loopback proof and the explicit bodyless foreign-response policy. The body now includes the real |
|
@clawsweeper re-review Updated the branch and PR body with declared-small/returned-large coverage. The focused test now covers a bodyless foreign response that declares |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
d80ac22 to
ce41bf0
Compare
|
Land-ready at exact head Deep review replaced the original Proof:
The body-less malformed path intentionally yields the original HTTP 400 diagnostic without an eager body read. |
ce41bf0 to
e3ac680
Compare
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
e3ac680 to
8240e57
Compare
|
Merged via squash.
|
* fix(agents): bound body-less MCP HTTP text responses * fix(agents): fail closed on unbounded MCP text fallback * test(agents): cover lying MCP bodyless content length * fix(agents): reject malformed MCP content length * chore: drop unused FinalizationRegistry after rebase Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(mcp): consolidate bounded response cases Co-authored-by: Pick-cat <huang.ting3@xydigit.com> * fix(mcp): reject unbounded bodyless responses Co-authored-by: Pick-cat <huang.ting3@xydigit.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): bound body-less MCP HTTP text responses * fix(agents): fail closed on unbounded MCP text fallback * test(agents): cover lying MCP bodyless content length * fix(agents): reject malformed MCP content length * chore: drop unused FinalizationRegistry after rebase Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(mcp): consolidate bounded response cases Co-authored-by: Pick-cat <huang.ting3@xydigit.com> * fix(mcp): reject unbounded bodyless responses Co-authored-by: Pick-cat <huang.ting3@xydigit.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): guard null response body in proxy stream reader The proxy transport used response.body! (non-null assertion) to enter the SSE read loop without checking whether the body is present. response.ok does not guarantee response.body is non-null — empty 200 responses and prematurely-closed connections leave body=null while ok=true. Sibling transports in the same repository already guard this case: - provider-transport-fetch.ts:148,385,541 - anthropic-transport-stream.ts:847 - openai-completions-transport.ts:272 - mcp-http-fetch.ts:60,76 (added in #98143) Add the same guard before getReader() so the caller receives a business- level diagnostic ("Proxy returned an empty response body") instead of the misleading JS runtime error "Cannot read properties of null (reading 'getReader')". The try-catch in streamProxy already catches the error and pushes it as a stream event, so this does not introduce a new crash — it replaces an internal TypeError with a meaningful error. * test(agents): cover null response body in proxy stream reader
…w#107325) * fix(agents): guard null response body in proxy stream reader The proxy transport used response.body! (non-null assertion) to enter the SSE read loop without checking whether the body is present. response.ok does not guarantee response.body is non-null — empty 200 responses and prematurely-closed connections leave body=null while ok=true. Sibling transports in the same repository already guard this case: - provider-transport-fetch.ts:148,385,541 - anthropic-transport-stream.ts:847 - openai-completions-transport.ts:272 - mcp-http-fetch.ts:60,76 (added in openclaw#98143) Add the same guard before getReader() so the caller receives a business- level diagnostic ("Proxy returned an empty response body") instead of the misleading JS runtime error "Cannot read properties of null (reading 'getReader')". The try-catch in streamProxy already catches the error and pushes it as a stream event, so this does not introduce a new crash — it replaces an internal TypeError with a meaningful error. * test(agents): cover null response body in proxy stream reader
What Problem This Solves
The MCP HTTP wrapper converted body-less foreign
Responseimplementations by callingtext(). That API materializes the complete remote body before OpenClaw can enforce a byte limit, so a hostile or compressed OAuth error response could exhaust memory.Closes #97521.
Why This Change Was Made
This takes the issue's fail-closed policy option. A body-less foreign response has no genuinely bounded reader:
Content-Lengthis remote-controlled and can describe compressed wire bytes rather than decoded text. The wrapper now preserves status and headers but never callstext()orarrayBuffer()for that shape. Normal responses with readable body streams are unchanged.The test rewrite removes repetitive response classes and covers missing, plausible, oversized, and dishonest
Content-Lengthvalues. MCP SDK source was checked directly:parseErrorResponsereads the globalResponse.text(), so the synthesized empty body still produces a useful HTTP-status diagnostic without touching the foreign unbounded method.User Impact
Malformed/nonconforming body-less MCP OAuth responses now report a generic status-based OAuth error instead of preserving their raw error text. This is intentional: it removes the memory-exhaustion path while retaining status and headers. Standards-compliant streamed responses retain full error parsing.
Evidence
tbx_01kwt6qx4r4ptdbg4ps3yhnj55:src/agents/mcp-http-fetch.test.ts, 13/13 passed.oxfmt --checkpassed for the changed source, tests, and changelog.git diff --checkpassed.No docs change is needed; the Unreleased changelog entry is included.