From fde2a2a37a1a4a87213d86ab2a948580b2f06c1d Mon Sep 17 00:00:00 2001 From: alexbuuuuuu Date: Tue, 8 Sep 2026 19:37:07 +0800 Subject: [PATCH 1/2] fix(opencode): surface 2xx-but-not-200 HTTP status in webfetch output webfetch only hands the model `output`; `title` and `metadata` are consumed by the UI and never reach it. A 2xx response that is not 200 - a 202 Accepted throttle page, a 204 No Content - therefore arrives looking like the requested content, and the model has no way to tell it should try another source. Prefix `output` with an "[HTTP ...]" notice whenever the status is not 200, and record the status in `metadata`. 200 responses are untouched, so existing output is unchanged. Non-2xx already surfaces its status through the error from filterStatusOk, so this closes the remaining gap. Fixes #47897 --- packages/opencode/src/tool/webfetch.ts | 34 ++++++++------ packages/opencode/src/tool/webfetch.txt | 1 + packages/opencode/test/tool/webfetch.test.ts | 49 ++++++++++++++++++++ 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index e6150345459e..63212dcb82d8 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -107,12 +107,24 @@ export const WebFetchTool = Tool.define( const mime = contentType.split(";")[0]?.trim().toLowerCase() || "" const title = `${params.url} (${contentType})` + // A non-200 success (202 Accepted, 204 No Content, ...) often carries a throttle + // or interstitial page instead of the requested content. Only `output` reaches the + // model, so the status has to be stated there for it to adapt. + const statusNotice = + response.status === 200 + ? "" + : `[HTTP ${response.status} — response may be a throttle or interstitial page, not the requested content]\n\n` + + const result = (output: string) => ({ + output: statusNotice + output, + title, + metadata: { status: response.status }, + }) + if (isImageAttachment(mime)) { const base64Content = Buffer.from(arrayBuffer).toString("base64") return { - title, - output: "Image fetched successfully", - metadata: {}, + ...result("Image fetched successfully"), attachments: [ { type: "file" as const, @@ -130,25 +142,21 @@ export const WebFetchTool = Tool.define( case "markdown": if (contentType.includes("text/html")) { const markdown = convertHTMLToMarkdown(content) - return { - output: markdown, - title, - metadata: {}, - } + return result(markdown) } - return { output: content, title, metadata: {} } + return result(content) case "text": if (contentType.includes("text/html")) { - return { output: extractTextFromHTML(content), title, metadata: {} } + return result(extractTextFromHTML(content)) } - return { output: content, title, metadata: {} } + return result(content) case "html": - return { output: content, title, metadata: {} } + return result(content) default: - return { output: content, title, metadata: {} } + return result(content) } }).pipe(Effect.orDie), } diff --git a/packages/opencode/src/tool/webfetch.txt b/packages/opencode/src/tool/webfetch.txt index 169aadefa368..b7f44d7c3088 100644 --- a/packages/opencode/src/tool/webfetch.txt +++ b/packages/opencode/src/tool/webfetch.txt @@ -11,3 +11,4 @@ Usage notes: - Format options: "markdown" (default), "text", or "html" - This tool is read-only and does not modify any files - Results may be summarized if the content is very large + - If the response status is not 200, the output starts with an "[HTTP ...]" notice; the body may be a throttle or interstitial page rather than the requested content diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts index 05599a6784e6..6302c755b16e 100644 --- a/packages/opencode/test/tool/webfetch.test.ts +++ b/packages/opencode/test/tool/webfetch.test.ts @@ -98,6 +98,55 @@ describe("tool.webfetch", () => { ), ) + it.instance("notes a non-200 status ahead of the body", () => + withFetch( + () => + new Response("

throttled

", { + status: 202, + headers: { "content-type": "text/html; charset=utf-8" }, + }), + (url) => + Effect.gen(function* () { + const result = yield* exec({ url: new URL("/page.html", url).toString(), format: "text" }) + expect(result.output.startsWith("[HTTP 202")).toBe(true) + expect(result.output).toContain("throttled") + expect(result.metadata.status).toBe(202) + }), + ), + ) + + it.instance("surfaces the notice alone when a throttled response has no body", () => + withFetch( + () => + new Response("", { + status: 202, + headers: { "content-type": "text/html; charset=utf-8" }, + }), + (url) => + Effect.gen(function* () { + const result = yield* exec({ url: new URL("/page.html", url).toString(), format: "text" }) + expect(result.output.startsWith("[HTTP 202")).toBe(true) + expect(result.output.trim().endsWith("]")).toBe(true) + }), + ), + ) + + it.instance("leaves 200 responses unannotated", () => + withFetch( + () => + new Response("

hello

", { + status: 200, + headers: { "content-type": "text/html; charset=utf-8" }, + }), + (url) => + Effect.gen(function* () { + const result = yield* exec({ url: new URL("/page.html", url).toString(), format: "text" }) + expect(result.output).toBe("hello") + expect(result.metadata.status).toBe(200) + }), + ), + ) + it.instance("extracts text from html without scripts or styles", () => withFetch( () => From 1b11fadfc548fa882119d12c3143d7bfbfd945dc Mon Sep 17 00:00:00 2001 From: alexbuuuuuu Date: Tue, 8 Sep 2026 20:29:40 +0800 Subject: [PATCH 2/2] docs(opencode): note that the status notice is plain text and precedes the body The notice is prefixed regardless of the requested format, so `format: "html"` no longer returns markup alone when the status is not 200. Say so in the tool description instead of leaving callers to assume the output is strictly the requested format. --- packages/opencode/src/tool/webfetch.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/webfetch.txt b/packages/opencode/src/tool/webfetch.txt index b7f44d7c3088..0b6f247088e4 100644 --- a/packages/opencode/src/tool/webfetch.txt +++ b/packages/opencode/src/tool/webfetch.txt @@ -1,7 +1,7 @@ - Fetches content from a specified URL - Takes a URL and optional format as input - Fetches the URL content, converts to requested format (markdown by default) -- Returns the content in the specified format +- Returns the content in the specified format, preceded by a plain-text status notice if the response status is not 200 - Use this tool when you need to retrieve and analyze web content Usage notes: @@ -11,4 +11,4 @@ Usage notes: - Format options: "markdown" (default), "text", or "html" - This tool is read-only and does not modify any files - Results may be summarized if the content is very large - - If the response status is not 200, the output starts with an "[HTTP ...]" notice; the body may be a throttle or interstitial page rather than the requested content + - If the response status is not 200, a plain-text "[HTTP ...]" line precedes the body regardless of the requested format; the body may be a throttle or interstitial page rather than the requested content