Skip to content

fix(agent): web_fetch returns the real body; zero-byte writes fail loudly - #803

Open
sweetmantech wants to merge 1 commit into
mainfrom
fix/web-fetch-empty-body
Open

fix(agent): web_fetch returns the real body; zero-byte writes fail loudly#803
sweetmantech wants to merge 1 commit into
mainfrom
fix/web-fetch-empty-body

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes the web_fetch row of chat#1918. No docs change — web_fetch is an internal sandbox agent tool with no OpenAPI contract.

The bug

webFetchTool.ts L66 captured the response body with bash process substitution:

"-o", `>(head -c ${MAX_BODY_LENGTH} >&3)`,

That does not expand in the sandbox's exec shell. curl writes to a file literally named >(head -c 10000 >&3), fails, and exits 23. L100 then whitelisted exit 23 as "expected truncation" and L117 set truncated: true, so a hard write failure was reported as { success: true, status: 200, body: "", truncated: true }.

Every URL returned an empty body with a plausible status. Agents concluded the whole internet was unreachable — including docs.recoupable.dev, so they could not even discover the API surface.

Proof, under the actual shell

New command:

exit=0
200
559
<!doctype html><html lang="en"><head><title>Example Domain</title>…

Old command, same shell:

exit=1
sh: syntax error near unexpected token `('

Observed identically in a live sandbox run on 2026-07-30 (chat 329c4760-646b-4749-a7e7-82fd64f9cf72): bash curl https://example.com returned HTTP 200 / 559 bytes, while web_fetch on the same URL returned {"body": "", "status": 200, "success": true, "truncated": true}.

The fix

  • Body goes to a mktemp file. stdout contract is status\nbyteSize\n<body> — the real size survives even though the body is capped at MAX_BODY_LENGTH for context.
  • truncated is derived from the byte size, not curl's exit code.
  • A nonzero exit that produced zero bytes returns success: false with stderr, instead of a fake 200.
  • Body is parsed from the third line onward, so bodies containing newlines are preserved (previously it split on the last newline).

Tests

RED→GREEN, 7 tests in lib/agent/tools/__tests__/webFetchTool.test.ts (5 were RED before the change):

test covers
parses status, size and body on success happy path
preserves a body that itself contains newlines old last-newline split bug
marks truncated by BYTE SIZE, not by exit code fix #2
returns success:false when curl exits 23 with an empty body the regression this PR exists for
returns success:false on any nonzero exit with an empty body fix #3
does not use bash process substitution guards the root cause
passes the request body for POST unchanged behavior

The previous test suite asserted the buggy contract (exit 23 → success: true); that assertion is replaced.

  • pnpm exec vitest run lib/agent50 files, 310 tests, all pass
  • pnpm exec eslint on both files → clean
  • pnpm exec tsc --noEmit → the 3 TS2769 errors on webFetchTool.ts L48-50 are pre-existing on main (verified by stashing: identical lines and count), untouched by this change

Remaining verification

Done-when in the issue also asks for confirmation inside a real sandbox run. The local POSIX-sh repro above reproduces the failure and the fix exactly, but I have not yet exercised the deployed preview — worth doing before merge.

🤖 Generated with Claude Code


Summary by cubic

Fixes the web_fetch sandbox tool so it returns real response bodies and treats zero-byte writes as failures. Resolves the regression where every URL appeared as an empty 200 (chat#1918).

  • Bug Fixes
    • Write response to mktemp and emit status\nbyteSize\n<body> on stdout.
    • Derive truncated from byte size, not curl exit code.
    • Return success: false on any nonzero exit when size is 0; include stderr.
    • Parse the body from the third line to preserve newlines.
    • Add tests covering status/size/body parsing, truncation, zero-byte exits, POST body, and guarding against process substitution.

Written for commit 0d51a4c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved web content retrieval reliability across different environments.
    • HTTP status codes are now validated more accurately, including failed or unavailable responses.
    • Responses are truncated based on the actual downloaded size, improving consistency for large pages.
    • Requests that return no content now report failure instead of appearing successful.

…udly

webFetchTool captured the response via bash process substitution
(-o >(head -c N >&3) with fd-3 juggling). That is a bash-ism and does not
expand in the sandbox's exec shell, so curl wrote to a file literally named
">(head -c 10000 >&3)", failed, and exited 23 with zero bytes. The exit-23
branch then mapped that to { success: true, truncated: true }, so every URL
came back as an empty-bodied 200 and agents concluded the web was blocked.

- Write the body to a mktemp file; stdout contract is `status\nbyteSize\n<body>`.
- truncated is derived from the real byte size, not from curl's exit code.
- A nonzero exit that produced no bytes now returns success: false with stderr.

Verified under POSIX sh (the sandbox exec shell): the new command returns
exit 0 / status 200 / 559 bytes for example.com, while the old one fails with
"syntax error near unexpected token `('".

chat#1918
@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 31, 2026 12:23am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

webFetchTool now captures curl output through a temporary file, reports HTTP status and response size separately, validates parsed metadata, and determines failures and truncation from response bytes.

Changes

Web fetch response handling

Layer / File(s) Summary
Capture and parse curl responses
lib/agent/tools/webFetchTool.ts
Curl output is written to a temporary file, metadata is emitted before the capped body, exit codes are preserved, invalid or 000 statuses become null, empty nonzero responses fail, and truncation uses actual response size.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • recoupable/chat issue 1839 — The change implements the described webFetchTool.ts fix using temporary-file capture and failure handling for empty nonzero responses.

Poem

Curl whispers into a file,
Bytes line up in measured style.
Status guards the waiting door,
Empty failures count once more.
Capped replies now know their size.

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The changes address web_fetch behavior but provide no evidence for linked issues #2 or #3, which require image API schema and Arweave upload work. Link this PR to the web_fetch issue or include the required inputSchema, image generation, Arweave upload, URL utility, and output schema changes.
Out of Scope Changes check ⚠️ Warning The webFetchTool changes are unrelated to linked issues #2 and #3, whose scope is image API schema organization and Arweave-backed image generation. Remove the web_fetch changes from this PR or update its linked issues to accurately cover the web_fetch fix.
Solid & Clean Code ⚠️ Warning webFetchTool's execute handler spans 87 lines and combines request construction, shell execution, stdout parsing, truncation, and error handling, violating SRP/clean-code size guidance. Extract command construction, response parsing, and fetch-result/error handling into focused helpers (with appropriately named files if exported) so execute coordinates the workflow.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/web-fetch-empty-body

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/agent/tools/webFetchTool.ts (1)

81-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split the command construction and output parsing from execute.

The execute callback is 88 lines (Lines 50-137), exceeding the 20-line limit and combining argument construction, shell protocol generation, and response parsing. Extract focused helpers such as buildFetchCommand and parseFetchOutput.

As per coding guidelines, “Flag functions longer than 20 lines” and “Keep functions small and focused.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/agent/tools/webFetchTool.ts` around lines 81 - 96, Refactor the oversized
execute callback by extracting argument/command construction into a focused
buildFetchCommand helper and stdout protocol parsing into parseFetchOutput. Keep
execute responsible only for invoking the command and coordinating the existing
fetch response and error behavior, while preserving the status\nbyteSize\nbody
contract and temporary-file handling.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/agent/tools/webFetchTool.ts`:
- Line 93: Align the web fetch tool’s public description and naming around the
byte-based truncation performed by `head -c` and `MAX_BODY_LENGTH`. Update the
description near the command construction to state a byte limit, or replace the
truncation with character-safe handling; keep the configured cap and
returned-body behavior consistent.
- Around line 117-123: Update the fetch result handling around the curl
exit-code check so every nonzero result.exitCode returns success: false,
regardless of size or partial response content. Preserve the existing error
message and ensure successful responses are only returned when curl exits with
code zero.

---

Nitpick comments:
In `@lib/agent/tools/webFetchTool.ts`:
- Around line 81-96: Refactor the oversized execute callback by extracting
argument/command construction into a focused buildFetchCommand helper and stdout
protocol parsing into parseFetchOutput. Keep execute responsible only for
invoking the command and coordinating the existing fetch response and error
behavior, while preserving the status\nbyteSize\nbody contract and
temporary-file handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 54718a5d-5707-48ee-9802-acfb8cb56b25

📥 Commits

Reviewing files that changed from the base of the PR and between f38823c and 0d51a4c.

⛔ Files ignored due to path filters (1)
  • lib/agent/tools/__tests__/webFetchTool.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/agent/tools/webFetchTool.ts

"printf '\\n%s' \"$status\"",
'size=$(wc -c < "$tmp" | tr -d " ")',
'printf \'%s\\n%s\\n\' "$status" "$size"',
`head -c ${MAX_BODY_LENGTH} "$tmp"`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the documented character cap with the byte-based implementation.

head -c caps bytes, while the tool description promises MAX_BODY_LENGTH characters. Non-ASCII bodies can be truncated early or split mid-character. Define the limit as bytes in the public description/name, or apply a character-safe cap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/agent/tools/webFetchTool.ts` at line 93, Align the web fetch tool’s
public description and naming around the byte-based truncation performed by
`head -c` and `MAX_BODY_LENGTH`. Update the description near the command
construction to state a byte limit, or replace the truncation with
character-safe handling; keep the configured cap and returned-body behavior
consistent.

Comment on lines +117 to 123
if (result.exitCode !== 0 && size === 0) {
return {
success: false,
error: `Fetch failed: ${result.stderr || result.stdout || "Unknown error"}`,
error: `Fetch failed (exit ${result.exitCode}): ${
result.stderr || "no response body written"
}`,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Treat every nonzero curl exit as a failed fetch.

A timeout or transport failure can write a partial body before curl exits nonzero. This branch currently returns success: true whenever size > 0, without indicating that the response is incomplete. With output redirected to a temp file, exit 23 is no longer the expected truncation mechanism.

Proposed fix
-      if (result.exitCode !== 0 && size === 0) {
+      if (result.exitCode !== 0) {
         return {
           success: false,
           error: `Fetch failed (exit ${result.exitCode}): ${
             result.stderr || "no response body written"
           }`,
         };
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (result.exitCode !== 0 && size === 0) {
return {
success: false,
error: `Fetch failed: ${result.stderr || result.stdout || "Unknown error"}`,
error: `Fetch failed (exit ${result.exitCode}): ${
result.stderr || "no response body written"
}`,
};
if (result.exitCode !== 0) {
return {
success: false,
error: `Fetch failed (exit ${result.exitCode}): ${
result.stderr || "no response body written"
}`,
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/agent/tools/webFetchTool.ts` around lines 117 - 123, Update the fetch
result handling around the curl exit-code check so every nonzero result.exitCode
returns success: false, regardless of size or partial response content. Preserve
the existing error message and ensure successful responses are only returned
when curl exits with code zero.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 2 files

Confidence score: 2/5

  • In lib/agent/tools/webFetchTool.ts, download limiting is applied only after curl finishes writing, so very large responses can fill the sandbox disk before truncation and destabilize tool execution — enforce a hard byte cap during transfer and preserve a clear truncated signal.
  • In lib/agent/tools/webFetchTool.ts, treating partial files from interrupted/nonzero-exit curl runs as successful can return incomplete content as if it were valid, which can mislead downstream agent decisions — gate success on zero exit status or return an explicit partial/error state.
  • In lib/agent/tools/webFetchTool.ts, byte-based truncation (head -c) can split multibyte UTF-8 characters if MAX_BODY_LENGTH is documented as character-based, leading to malformed text in responses — align docs to byte semantics or switch to character-safe truncation.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/agent/tools/webFetchTool.ts">

<violation number="1" location="lib/agent/tools/webFetchTool.ts:66">
P1: Large responses can exhaust sandbox disk because truncation happens only after curl has fully written the temp file. Bound bytes while downloading, while retaining a reliable truncated indicator.</violation>

<violation number="2" location="lib/agent/tools/webFetchTool.ts:93">
P3: `head -c ${MAX_BODY_LENGTH} "$tmp"` truncates by bytes, but if the tool's description documents `MAX_BODY_LENGTH` as a character limit, non-ASCII response bodies can be truncated mid-character, producing invalid UTF-8 in the returned body. Consider clarifying the limit as byte-based in the docs, or using a character-safe truncation approach.</violation>

<violation number="3" location="lib/agent/tools/webFetchTool.ts:117">
P1: Interrupted/failed transfers that wrote a partial body are now reported as successful responses. Treat nonzero curl exits as failures (or explicitly expose partial/error state) rather than using byte count as the success criterion.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

String(Math.ceil(FETCH_TIMEOUT_MS / 1000)),
"-o",
`>(head -c ${MAX_BODY_LENGTH} >&3)`,
'"$tmp"',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Large responses can exhaust sandbox disk because truncation happens only after curl has fully written the temp file. Bound bytes while downloading, while retaining a reliable truncated indicator.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agent/tools/webFetchTool.ts, line 66:

<comment>Large responses can exhaust sandbox disk because truncation happens only after curl has fully written the temp file. Bound bytes while downloading, while retaining a reliable truncated indicator.</comment>

<file context>
@@ -63,7 +63,7 @@ USAGE:
       String(Math.ceil(FETCH_TIMEOUT_MS / 1000)),
       "-o",
-      `>(head -c ${MAX_BODY_LENGTH} >&3)`,
+      '"$tmp"',
       "-w",
       shellEscape("%{http_code}"),
</file context>

// A nonzero exit that produced no bytes is a hard failure. Reporting it as
// an empty-bodied 200 is what made agents conclude the whole web was
// unreachable; surface it so they retry or report instead.
if (result.exitCode !== 0 && size === 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Interrupted/failed transfers that wrote a partial body are now reported as successful responses. Treat nonzero curl exits as failures (or explicitly expose partial/error state) rather than using byte count as the success criterion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agent/tools/webFetchTool.ts, line 117:

<comment>Interrupted/failed transfers that wrote a partial body are now reported as successful responses. Treat nonzero curl exits as failures (or explicitly expose partial/error state) rather than using byte count as the success criterion.</comment>

<file context>
@@ -96,25 +101,34 @@ USAGE:
+      // A nonzero exit that produced no bytes is a hard failure. Reporting it as
+      // an empty-bodied 200 is what made agents conclude the whole web was
+      // unreachable; surface it so they retry or report instead.
+      if (result.exitCode !== 0 && size === 0) {
         return {
           success: false,
</file context>
Suggested change
if (result.exitCode !== 0 && size === 0) {
if (result.exitCode !== 0) {

"printf '\\n%s' \"$status\"",
'size=$(wc -c < "$tmp" | tr -d " ")',
'printf \'%s\\n%s\\n\' "$status" "$size"',
`head -c ${MAX_BODY_LENGTH} "$tmp"`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: head -c ${MAX_BODY_LENGTH} "$tmp" truncates by bytes, but if the tool's description documents MAX_BODY_LENGTH as a character limit, non-ASCII response bodies can be truncated mid-character, producing invalid UTF-8 in the returned body. Consider clarifying the limit as byte-based in the docs, or using a character-safe truncation approach.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agent/tools/webFetchTool.ts, line 93:

<comment>`head -c ${MAX_BODY_LENGTH} "$tmp"` truncates by bytes, but if the tool's description documents `MAX_BODY_LENGTH` as a character limit, non-ASCII response bodies can be truncated mid-character, producing invalid UTF-8 in the returned body. Consider clarifying the limit as byte-based in the docs, or using a character-safe truncation approach.</comment>

<file context>
@@ -78,15 +78,20 @@ USAGE:
-      "printf '\\n%s' \"$status\"",
+      'size=$(wc -c < "$tmp" | tr -d " ")',
+      'printf \'%s\\n%s\\n\' "$status" "$size"',
+      `head -c ${MAX_BODY_LENGTH} "$tmp"`,
+      'rm -f "$tmp"',
       "exit $curlExit",
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant