anomalyco / anomalyco/opencode

webfetch timeout does not cover the response body read

Open
#45,229 1 comment 0 reactions 1 assignee View on GitHub

@neriousy is already working on this.

Since Aug 26, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Description

webfetch's timeout is applied to the request but not to the body read, so a server that answers headers promptly and then stalls mid-body hangs forever. Two smaller defects sit alongside it in the same expression.

packages/opencode/src/tool/webfetch.ts:

const response = yield* httpOk.execute(request).pipe(
  Effect.catchIf(/* cloudflare retry */),
  Effect.timeoutOrElse({ duration: timeout, orElse: () => Effect.die(new Error("Request timed out")) }),
)

// Check content length
const contentLength = response.headers["content-length"]
...
const arrayBuffer = yield* response.arrayBuffer     // <-- outside the timeout

1. The timeout does not cover the body. timeoutOrElse closes over httpOk.execute(request) only. response.arrayBuffer runs after it, unguarded. Headers arriving within 30s satisfy the timeout no matter how long the body takes, and a slow-drip response never returns.

2. Effect.die instead of Effect.fail. A timeout is an expected outcome of fetching an arbitrary URL, not a defect. Effect.die raises it as an unrecoverable defect rather than a typed error the tool layer can turn into a message the model reads and acts on.

3. timeout has no lower bound. The parameter is Schema.optional(Schema.Number), so 0 and negatives are accepted. Math.min((params.timeout ?? 30) * 1000, MAX_TIMEOUT) clamps the ceiling but not the floor, so timeout: 0 produces a 0 ms deadline that aborts the request immediately and then dies per (2).

The v2 tool already gets all three right

packages/core/src/tool/webfetch.ts wraps the request and collectBody(response) in one Effect.gen before applying the timeout, fails instead of dying, and bounds the input:

}).pipe(
  Effect.timeoutOrElse({
    duration: Duration.seconds(input.timeout ?? DEFAULT_TIMEOUT_SECONDS),
    orElse: () => Effect.fail(new Error("Request timed out")),
  }),
)
const Timeout = Schema.Number.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(MAX_TIMEOUT_SECONDS))

The registered v1 tool is the one that predates it.

Steps to reproduce
  1. Point webfetch at an endpoint that returns headers immediately and then writes the body slowly (or not at all).
  2. The call does not return after the timeout elapses.

For (3): call webfetch with timeout: 0 on any URL — the request is aborted immediately.

Operating System

Windows 11 (platform independent)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.