Skip to content

fix(webfetch): keep body read inside the timeout scope to stop "context canceled"#59

Merged
ysyneu merged 1 commit into
mainfrom
fix/webfetch-context-canceled
Jun 9, 2026
Merged

fix(webfetch): keep body read inside the timeout scope to stop "context canceled"#59
ysyneu merged 1 commit into
mainfrom
fix/webfetch-context-canceled

Conversation

@ysyneu

@ysyneu ysyneu commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

webfetch against large/slow pages (reported on Aliyun help docs, e.g. https://help.aliyun.com/zh/openapi/user-guide/openapi-mcp-server-guide) fails with:

remote webfetch operation failed: failed to read response: context canceled

The user set timeout: 60, yet the error is context canceled, not context deadline exceeded — so it was never a real timeout. It's a premature context cancel.

Root cause

fetchURL created a per-request timeout context and defer cancel()'d it, then returned the *http.Response:

func (e *Environment) fetchURL(...) (*http.Response, error) {
    httpCtx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()                 // fires when fetchURL RETURNS
    ...
    return fetchClient.Do(req)     // cancel() runs here, killing httpCtx
}

WebFetch then called readResponseBody(resp) after fetchURL returned. net/http ties the lifetime of resp.Body to the request context, so the io.ReadAll aborts with context canceled for any response still streaming when Do() returned. Large pages trip this every time; small pages got lucky only because their body was already buffered when Do() returned.

Safari's own localWebFetch (the support-agent path) does it correctly — Do() and ReadAll share a single defer cancel() scope. The runner had split them across the cancel boundary.

Fix

Merge the request and the body read into one timeout-scoped function (fetchBody), so the cancel cannot fire before the read. This makes the bug class structurally impossible. processor.Process still runs on the outer ctx (body is already in memory, must not be bound to the per-fetch timeout).

Test

TestFetchBody_ReadsSlowStreamedBody streams a body slowly (so it cannot be pre-buffered) and asserts the full body is read. Verified it reproduces the exact failure against the old split structure (failed to read response: context canceled) and passes against the fix.

🤖 Generated with Claude Code

…xt canceled"

fetchURL created a per-request timeout context and deferred its cancel, then
returned the *http.Response. The cancel fired the moment fetchURL returned —
before WebFetch read the body via readResponseBody. net/http ties the body
read to the request context, so any response still streaming when Do()
returned aborted with "failed to read response: context canceled".

Large/slow pages (e.g. Aliyun help docs) hit this every time because their
body is still in flight when Do() returns; small pages got lucky only because
their body was already buffered. The error is "context canceled", not
"deadline exceeded" — it was never a real timeout, just a premature cancel.

Merge the request and the body read into one timeout-scoped function
(fetchBody), mirroring Safari's correct localWebFetch, so the cancel cannot
fire before the read. Add a regression test that streams a slow body and
asserts it is read in full (reproduces the exact error against the old
structure).
@ysyneu ysyneu merged commit 4e4cf6f into main Jun 9, 2026
10 checks passed
@ysyneu ysyneu deleted the fix/webfetch-context-canceled branch June 9, 2026 07:31
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