summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-04-07 15:46:02 -0500
committerGitHub <[email protected]>2026-04-07 20:46:02 +0000
commitbc1840b196dcc9d438861e53301a3dbbeab6974f (patch)
tree078651409402652c482a1b728bdc1796e2c3f224
parent095aeba0a77482f3061e623c7d91c29da76f195e (diff)
downloadopencode-bc1840b196dcc9d438861e53301a3dbbeab6974f.tar.gz
opencode-bc1840b196dcc9d438861e53301a3dbbeab6974f.zip
fix(opencode): clear webfetch timeouts on failed fetches (#21378)
-rw-r--r--packages/opencode/src/tool/webfetch.ts20
-rw-r--r--packages/opencode/test/tool/webfetch.test.ts53
2 files changed, 65 insertions, 8 deletions
diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts
index a66e66c09..559afd677 100644
--- a/packages/opencode/src/tool/webfetch.ts
+++ b/packages/opencode/src/tool/webfetch.ts
@@ -3,6 +3,7 @@ import { Tool } from "./tool"
import TurndownService from "turndown"
import DESCRIPTION from "./webfetch.txt"
import { abortAfterAny } from "../util/abort"
+import { iife } from "@/util/iife"
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
@@ -62,15 +63,18 @@ export const WebFetchTool = Tool.define("webfetch", {
"Accept-Language": "en-US,en;q=0.9",
}
- const initial = await fetch(params.url, { signal, headers })
+ const response = await iife(async () => {
+ try {
+ const initial = await fetch(params.url, { signal, headers })
- // Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch)
- const response =
- initial.status === 403 && initial.headers.get("cf-mitigated") === "challenge"
- ? await fetch(params.url, { signal, headers: { ...headers, "User-Agent": "opencode" } })
- : initial
-
- clearTimeout()
+ // Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch)
+ return initial.status === 403 && initial.headers.get("cf-mitigated") === "challenge"
+ ? await fetch(params.url, { signal, headers: { ...headers, "User-Agent": "opencode" } })
+ : initial
+ } finally {
+ clearTimeout()
+ }
+ })
if (!response.ok) {
throw new Error(`Request failed with status code: ${response.status}`)
diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts
index 088f3dd16..c37ba3e08 100644
--- a/packages/opencode/test/tool/webfetch.test.ts
+++ b/packages/opencode/test/tool/webfetch.test.ts
@@ -17,6 +17,8 @@ const ctx = {
ask: async () => {},
}
+type TimerID = ReturnType<typeof setTimeout>
+
async function withFetch(
mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
fn: () => Promise<void>,
@@ -30,6 +32,32 @@ async function withFetch(
}
}
+async function withTimers(fn: (state: { ids: TimerID[]; cleared: TimerID[] }) => Promise<void>) {
+ const set = globalThis.setTimeout
+ const clear = globalThis.clearTimeout
+ const ids: TimerID[] = []
+ const cleared: TimerID[] = []
+
+ globalThis.setTimeout = ((...args: Parameters<typeof setTimeout>) => {
+ const id = set(...args)
+ ids.push(id)
+ return id
+ }) as typeof setTimeout
+
+ globalThis.clearTimeout = ((id?: TimerID) => {
+ if (id !== undefined) cleared.push(id)
+ return clear(id)
+ }) as typeof clearTimeout
+
+ try {
+ await fn({ ids, cleared })
+ } finally {
+ ids.forEach(clear)
+ globalThis.setTimeout = set
+ globalThis.clearTimeout = clear
+ }
+}
+
describe("tool.webfetch", () => {
test("returns image responses as file attachments", async () => {
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
@@ -98,4 +126,29 @@ describe("tool.webfetch", () => {
},
)
})
+
+ test("clears timeout when fetch rejects", async () => {
+ await withTimers(async ({ ids, cleared }) => {
+ await withFetch(
+ async () => {
+ throw new Error("boom")
+ },
+ async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const webfetch = await WebFetchTool.init()
+ await expect(
+ webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx),
+ ).rejects.toThrow("boom")
+ },
+ })
+ },
+ )
+
+ expect(ids).toHaveLength(1)
+ expect(cleared).toHaveLength(1)
+ expect(cleared[0]).toBe(ids[0])
+ })
+ })
})