[BUG] WebFetch hangs forever on a CSS-heavy 1 MB page
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
WebFetchhanging 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
- 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.")
- Wait. The call never returns; it has to be interrupted.
- 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.")
- To confirm the site is not the bottleneck, fetch the same URL outside the tool —
curland Node'sfetchboth 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
curland via Node'sfetch, repeatedly, before and after a hang. - Payload size on the way out — a 2.4 MB POST to
postman-echo.com/postcompletes in 2.8 s, so sending a body of the size involved here is not what stalls. - Permissions — a domain absent from
permissions.allowis 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
- Give the whole
WebFetchpipeline a deadline, including the summarization stream, and surface an error when it expires. Enabling the existing watchdog by default would cover this. - 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.
- 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.
3 Comments
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
curlof the HTML alone, same region | ~100 s |Two properties that map directly onto this bug:
<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:
Repo + self-hosted binary/Docker if you'd rather keep it local: https://github.com/yinnho/aginxbrowser
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 withclaude -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=WebFetchand then nothing - no[API REQUEST] ... source=web_fetch_apply, notool_dispatch_end. CPU stays ~0.CLAUDE_ENABLE_STREAM_WATCHDOG=1makes 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 withNO_PROXY=ossinsight.ioreturns 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 answersConnection: closerequests 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, neverweb_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.
@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 viaSSLSocket.detach()(no close_notify).| variant | result | time |
|---|---|---|
| complete body (
0\r\n\r\nsent) + 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.