fix(opencode): clear webfetch timeouts on failed fetches (#21378)
parent
095aeba0a7
commit
bc1840b196
|
|
@ -3,6 +3,7 @@ import { Tool } from "./tool"
|
||||||
import TurndownService from "turndown"
|
import TurndownService from "turndown"
|
||||||
import DESCRIPTION from "./webfetch.txt"
|
import DESCRIPTION from "./webfetch.txt"
|
||||||
import { abortAfterAny } from "../util/abort"
|
import { abortAfterAny } from "../util/abort"
|
||||||
|
import { iife } from "@/util/iife"
|
||||||
|
|
||||||
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
|
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
|
||||||
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
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",
|
"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)
|
// Retry with honest UA if blocked by Cloudflare bot detection (TLS fingerprint mismatch)
|
||||||
const response =
|
return initial.status === 403 && initial.headers.get("cf-mitigated") === "challenge"
|
||||||
initial.status === 403 && initial.headers.get("cf-mitigated") === "challenge"
|
? await fetch(params.url, { signal, headers: { ...headers, "User-Agent": "opencode" } })
|
||||||
? await fetch(params.url, { signal, headers: { ...headers, "User-Agent": "opencode" } })
|
: initial
|
||||||
: initial
|
} finally {
|
||||||
|
clearTimeout()
|
||||||
clearTimeout()
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Request failed with status code: ${response.status}`)
|
throw new Error(`Request failed with status code: ${response.status}`)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ const ctx = {
|
||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimerID = ReturnType<typeof setTimeout>
|
||||||
|
|
||||||
async function withFetch(
|
async function withFetch(
|
||||||
mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
|
mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
|
||||||
fn: () => Promise<void>,
|
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", () => {
|
describe("tool.webfetch", () => {
|
||||||
test("returns image responses as file attachments", async () => {
|
test("returns image responses as file attachments", async () => {
|
||||||
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
|
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])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue