From 367cf59612794cd5979dd6a79bb8d916ccf680e9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 27 Aug 2026 11:56:34 -0400 Subject: [PATCH] refactor(core): simplify WebFetch error narrowing (#45582) Use the typed HTTP error reason instead of manual object probing and an unchecked response cast. Preserve the single challenge-only retry and cover ordinary 403 failures without retry. --- packages/core/src/tool/plugin/webfetch.ts | 17 ++++------------- packages/core/test/tool-webfetch.test.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/core/src/tool/plugin/webfetch.ts b/packages/core/src/tool/plugin/webfetch.ts index 7d692a09e95..379fd336b62 100644 --- a/packages/core/src/tool/plugin/webfetch.ts +++ b/packages/core/src/tool/plugin/webfetch.ts @@ -3,7 +3,7 @@ export * as WebFetchTool from "./webfetch.js" import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Duration, Effect, Schema } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { Parser } from "htmlparser2" import { Permission } from "../../permission.js" import { convertHTMLToMarkdown, MAX_MARKDOWN_BYTES } from "../html-markdown.js" @@ -58,18 +58,9 @@ const headers = (format: Format, userAgent: string) => ({ const openCodeUserAgent = "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OpenCode-User/1.0; +https://opencode.ai" -const isCloudflareChallenge = (error: unknown) => { - if (!error || typeof error !== "object" || !("reason" in error)) return false - const reason = error.reason - if ( - !reason || - typeof reason !== "object" || - !("_tag" in reason) || - reason._tag !== "StatusCodeError" || - !("response" in reason) - ) - return false - const response = reason.response as HttpClientResponse.HttpClientResponse +const isCloudflareChallenge = (error: HttpClientError.HttpClientError) => { + if (error.reason._tag !== "StatusCodeError") return false + const response = error.reason.response return response.status === 403 && response.headers["cf-mitigated"] === "challenge" } diff --git a/packages/core/test/tool-webfetch.test.ts b/packages/core/test/tool-webfetch.test.ts index d746fddf2b1..1d5d73c1762 100644 --- a/packages/core/test/tool-webfetch.test.ts +++ b/packages/core/test/tool-webfetch.test.ts @@ -573,6 +573,22 @@ describe("WebFetchTool registration", () => { }), ) + it.effect("does not retry ordinary 403 responses", () => + Effect.gen(function* () { + reset() + respond = () => Effect.succeed(new Response("forbidden", { status: 403 })) + const registry = yield* Tool.Service + const url = "https://example.com/forbidden" + + expect(yield* executeTool(registry, call({ url, format: "text" }))).toEqual({ + status: "error", + error: { type: "unknown", message: `StatusCode: non 2xx status code (403 GET ${url})` }, + }) + expect(requests).toHaveLength(1) + expect(requests[0]?.headers["user-agent"]).toBe(webFetchUserAgent) + }), + ) + it.effect("times out stalled requests", () => Effect.gen(function* () { reset()