[BUG] WebFetch hangs forever on a CSS-heavy 1 MB page

Status Open
Reported on v2.1.105
Maintainer reply None cached
Activity 3 comments · opened Aug 15, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

WebFetch never returns for some large pages. There is no timeout, no retry and no error — the call waits indefinitely until it is interrupted by hand.

The page below reproduces it every time. Three attempts, none returned; one was left running for 450 s before interrupting, and an earlier occurrence ran for 51 minutes without returning.

Two changelog entries already target this area, and both shipped before the version I am on:

  • 2.1.105 — strip <style> and <script> contents from fetched pages so CSS-heavy pages no longer exhaust the content budget before reaching actual text
  • 2.1.117 — fixed WebFetch hanging on very large HTML pages by truncating input before HTML-to-markdown conversion

What makes this page different from other large pages is that it inlines an entire Tailwind stylesheet:

| | posthog.com/docs/data/events | en.wikipedia.org/wiki/Rust_(programming_language) |
| --- | --- | --- |
| HTML | 1061 KB | 983 KB |
| inline <style> | 598 KB (largest single block 597 KB) | 20 KB |
| visible text | 9 KB | 92 KB |
| WebFetch | hangs indefinitely | returns in a few seconds |

Both are roughly 1 MB of HTML, so raw size alone does not decide it. The one that works links its CSS externally.

While hung, the process is doing nothing at all: the page download completes, then no further bytes arrive, CPU stays near zero, the established connection count is flat, and the only traffic is a 302-byte keepalive every ~30 s. That matches the pipeline analysis in #34236, which reported that the final summarization step calls messages.create({stream: true}) with no timeout and that the stream watchdog is off unless CLAUDE_ENABLE_STREAM_WATCHDOG=1 is set.

Interrupting is also misreported: the tool result reads The user doesn't want to proceed with this tool use, which is indistinguishable from a real permission denial and sends debugging in the wrong direction.

What Should Happen?

WebFetch should either return a summary of the page or fail with an error within a bounded time. A stalled summarization stream should hit a deadline and surface that as an error, rather than leaving the tool call outstanding forever — an agent running unattended stalls until the session ends.

Error Messages/Logs

# No error is ever produced -- that is the bug. Sampling the Claude Code process
# every 2 s during a hang (PowerShell Get-Process, Win32_Process IO counters,
# Get-NetTCPConnection); offsets are relative to the WebFetch call:

t+0s     cpu=7%  conns=29  dRead=5,167,731B  dWrite=1,771,920B   <- page fetched
t+3s     cpu=2%  conns=28  dRead=0B          dWrite=0B
t+6s     cpu=1%  conns=29  dRead=0B          dWrite=0B
...
t+31s    cpu=2%  conns=28  dRead=0B          dWrite=302B         <- keepalive only
...
t+150s   cpu=1%  conns=28  dRead=0B          dWrite=303B
# still hung at this point; ended by user interrupt, reported as:
# "The user doesn't want to proceed with this tool use."

Steps to Reproduce

  1. Call the tool on a page that inlines a very large stylesheet:

``
WebFetch(url="https://posthog.com/docs/data/events",
prompt="Return the page's main heading only.")
``

  1. Wait. The call never returns; it has to be interrupted.
  1. For contrast, the same call against a page of comparable size but with external CSS returns in a few seconds:

``
WebFetch(url="https://en.wikipedia.org/wiki/Rust_(programming_language)",
prompt="Return the first sentence of the article only.")
``

  1. To confirm the site is not the bottleneck, fetch the same URL outside the tool — curl and Node's fetch both return 200 in under 4 s, repeatedly, before and after a hang.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.229 (Claude Code)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Other

Additional Information

Also ruled out

  • Network and the site — the same URL returns 200 in under 4 s via curl and via Node's fetch, repeatedly, before and after a hang.
  • Payload size on the way out — a 2.4 MB POST to postman-echo.com/post completes in 2.8 s, so sending a body of the size involved here is not what stalls.
  • Permissions — a domain absent from permissions.allow is auto-approved in this mode and returns normally, so the wait is not a pending permission prompt.
  • CPU-bound extraction such as catastrophic regex backtracking — CPU stays near zero throughout.

Asks

  1. Give the whole WebFetch pipeline a deadline, including the summarization stream, and surface an error when it expires. Enabling the existing watchdog by default would cover this.
  2. Work out why the 2.1.105 stripping and 2.1.117 truncation paths do not cover this page — a 1 MB page whose text content is 9 KB should be cheap to summarize after either step.
  3. Report an interrupt distinctly from a permission denial.

Related

  • #34236 — same symptom on a different URL, with the pipeline analysis quoted above; closed as stale by the inactivity bot and locked, so this is filed fresh
  • #11650, #8980, #10075 — earlier hang reports, all closed
  • #51783 — docs issue noting the 2.1.117 truncation behaviour is undocumented

About the version

This is Claude Code 2.1.229 as bundled in Claude Desktop 1.30096.5, which manages the CLI itself; the newest published Claude Code at the time of filing is 2.1.233, and I have no way to move the bundled copy forward by hand. No changelog entry after 2.1.117 touches WebFetch hangs, so I do not expect the newer build to behave differently.

View original on GitHub ↗

3 Comments

yinnho · 3 days ago

Reproduced your exact URL through a different fetch path in case it's useful as a workaround while this bug stands — and because the "no timeout exists at all" property is the part worth measuring against.

I maintain an MCP browser (Rust, built on the obscura engine) that Claude Code can use as its fetch layer. Your repro page, measured just now via our hosted instance:

| | |
|---|---|
| raw HTML | 1,091,216 bytes |
| output to the model | 8.7 KB markdown (title + content JSON) |
| cold tool call | 19.0 s (China-based server → US origin, includes TLS/stealth handshake) |
| warm tool call | 8.2 s |
| plain curl of the HTML alone, same region | ~100 s |

Two properties that map directly onto this bug:

  • No infinite-wait code path. Every network hop carries a hard 30 s request / 10 s connect timeout, and the conversion runs on an already-parsed DOM, not on serialized HTML of unbounded shape. Your 51-minute hang shouldn't be reachable from any fetch tool.
  • The stylesheet never reaches the model. <style>/<script> are stripped at the DOM layer before extraction, so an inlined full Tailwind build costs parse time but zero output budget — rather than truncating input upstream of the converter and hoping the cut lands after the text.

To be clear this doesn't replace fixing WebFetch — your two changelog citations show the conversion layer keeps finding unbounded inputs. But if you hit this page class regularly, routing those fetches through an MCP tool is one line:

claude mcp add aginxbrowser --transport http https://browser.aginx.net/mcp

Repo + self-hosted binary/Docker if you'd rather keep it local: https://github.com/yinnho/aginxbrowser

OYLFLMH · 18 hours ago

Adding a second, wire-level characterisation of "WebFetch never returns" on Windows: in our environment the hang is located before the summarisation call, and the trigger is a TLS record error arriving after a complete response body.

Environment: Claude Code 2.1.251 (VS Code extension native binary, --permission-mode auto), Windows 10, HTTPS_PROXY=http://127.0.0.1:10808 (sing-box -> remote xray). Also reproduced headless with claude -p --debug-file.

Symptom: WebFetch("https://ossinsight.io/blog/agent-memory-race-2026") never returns; a second page on the same host (/blog/design-md-protocol-2026) hangs identically. Interactive sessions stalled 13 h and 2 h respectively until Esc; the interrupt is reported as "User rejected tool use". Debug log shows [Stall] tool_dispatch_start tool=WebFetch and then nothing - no [API REQUEST] ... source=web_fetch_apply, no tool_dispatch_end. CPU stays ~0. CLAUDE_ENABLE_STREAM_WATCHDOG=1 makes no difference (the stall is in the page fetch, not the API stream). Other hosts through the same proxy (arxiv.org, mem0.ai, agent-plugins.org) return in 4-7 s. The same URL with NO_PROXY=ossinsight.io returns in 5.8 s.

Retrospective count: across 15 days (Aug 16-30) we count 8 hangs on 6 Vercel-hosted domains (ossinsight.io, www.mindstudio.ai, docs.open-metadata.org, recipes.vllm.ai, vllm.ai, artificialanalysis.ai). 6 occurred in workflow subagents (detected by transcript audit: the last tool_use in that branch never got a tool_result while sibling agents kept writing) and 2 in the main session. A hang on a 7th domain (docs.letta.com) was intercepted by a local guard on Aug 30 before the tool call was dispatched. The same URLs fetched direct (NO_PROXY) return normally.

What is different on the wire: our proxy chain appends, on every connection, one trailing TLS record (17 03 03 00 13 + 19 bytes) that does not verify under any of the session's keys (checked by decrypting the capture with SSLKEYLOGFILE) - i.e. it is injected by a proxy hop, not sent by the origin. It is harmless for origins that send close_notify before FIN (Cloudflare: the client stops reading at close_notify). Vercel answers Connection: close requests with the chunked body and then a bare TCP FIN, no close_notify - so the client reads the complete body and then hits a record that fails MAC verification (OpenSSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC). curl and Python simply error out after the complete body; Claude Code's fetch stage never resolves.

Local emulation (one data point, not yet a deterministic repro): a Python HTTPS server that replays the captured origin response verbatim (same chunk layout) and then immediately writes a bogus 19-byte record on the raw socket followed by FIN made claude.exe hang once (tool_dispatch_start, never web_fetch_apply, killed after 300 s). The same server reached through a local CONNECT relay one second earlier did not hang, and injecting the record 50 ms later did not either - so the client-side condition is timing/code-path sensitive and we could not pin it down from outside the binary. We can share the replay server and the captured response if useful.

Relation to the analysis above: our capture locates the stall before the summarisation call described in #34236 - both stages appear to lack a deadline, so the ask covers the fetch stage as well.

Ask (same as the original): give the fetch stage of WebFetch a deadline, and treat a TLS error/EOF that arrives after a complete body as end-of-response (or as a tool error) instead of leaving the promise pending forever.

yinnho · 17 hours ago

@OYLFLMH's wire pattern, replayed against a different HTTP/TLS stack for comparison. I maintain an MCP browser (Rust) whose fetch layer is wreq (BoringSSL) over hyper — commented above with the WebFetch workaround. Since your emulation suggests the vulnerable condition is "TLS error after complete body," I built a local mock and ran our stack against three variants of it.

Mock: Python TLS server, self-signed cert, sends a chunked response, then injects a bogus 19-byte record (17 03 03 00 13 + random payload) on the raw socket via SSLSocket.detach() (no close_notify).

| variant | result | time |
|---|---|---|
| complete body (0\r\n\r\n sent) + garbage record + bare FIN | 200, full 31-byte body delivered | ~12 ms cold, ~1.5 ms warm |
| complete body + garbage record, socket held open (no FIN, no close_notify) | 200, full body — the chunked decoder finishes at the terminal chunk and the garbage record is never even read | ~6 ms cold |
| incomplete body (terminal chunk withheld) + garbage record + FIN | clean error decoding response body: error reading a body from connection | ~4.5 ms |

So on this stack the pattern resolves immediately in all three shapes — the body framing (chunked terminator) bounds the read before the corrupt record matters, and when the body is genuinely incomplete the TLS error surfaces as a normal body error rather than a pending promise. There is also a hard 30 s total-request timeout above all of this as a backstop, but it never came into play — every run resolved in milliseconds.

This matches your reading that the hang is a client-side code-path bug, not something inherent to the wire pattern: curl, Python, and now wreq/hyper all treat "TLS error after complete body" as end-of-response or as an ordinary error. Claude Code's fetch stage is the outlier in leaving the promise pending.

Supporting the ask: a deadline on the fetch stage, plus treating a post-body TLS error as end-of-response, would cover both this and the summarisation-stage hang from #34236.