diff --git a/src/agents/tools/web-tools.fetch.test.ts b/src/agents/tools/web-tools.fetch.test.ts index da700ea537e..e9bfabbee7a 100644 --- a/src/agents/tools/web-tools.fetch.test.ts +++ b/src/agents/tools/web-tools.fetch.test.ts @@ -1,6 +1,7 @@ import { EnvHttpProxyAgent } from "undici"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as ssrf from "../../infra/net/ssrf.js"; +import { resolveRequestUrl } from "../../plugin-sdk/request-url.js"; import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { makeFetchHeaders } from "./web-fetch.test-harness.js"; import { createWebFetchTool } from "./web-tools.js"; @@ -76,19 +77,6 @@ function errorHtmlResponse( text: async () => html, }; } -function requestUrl(input: RequestInfo | URL): string { - if (typeof input === "string") { - return input; - } - if (input instanceof URL) { - return input.toString(); - } - if ("url" in input && typeof input.url === "string") { - return input.url; - } - return ""; -} - function installMockFetch( impl: (input: RequestInfo | URL, init?: RequestInit) => Promise, ) { @@ -122,7 +110,7 @@ function installPlainTextFetch(text: string) { status: 200, headers: makeFetchHeaders({ "content-type": "text/plain" }), text: async () => text, - url: requestUrl(input), + url: resolveRequestUrl(input), } as Response), ); } @@ -212,7 +200,7 @@ describe("web_fetch extraction fallbacks", () => { status: 200, headers: makeFetchHeaders({ "content-type": "text/plain" }), text: async () => longText, - url: requestUrl(input), + url: resolveRequestUrl(input), } as Response), ); @@ -274,7 +262,7 @@ describe("web_fetch extraction fallbacks", () => { status: 200, headers: makeFetchHeaders({ "content-type": "text/plain" }), text: async () => "proxy body", - url: requestUrl(input), + url: resolveRequestUrl(input), } as Response), ); const tool = createFetchTool({ firecrawl: { enabled: false } }); @@ -293,7 +281,7 @@ describe("web_fetch extraction fallbacks", () => { it("falls back to firecrawl when readability returns no content", async () => { installMockFetch((input: RequestInfo | URL) => { - const url = requestUrl(input); + const url = resolveRequestUrl(input); if (url.includes("api.firecrawl.dev")) { return Promise.resolve(firecrawlResponse("firecrawl content")) as Promise; } @@ -311,7 +299,7 @@ describe("web_fetch extraction fallbacks", () => { it("normalizes firecrawl Authorization header values", async () => { const fetchSpy = installMockFetch((input: RequestInfo | URL) => { - const url = requestUrl(input); + const url = resolveRequestUrl(input); if (url.includes("api.firecrawl.dev/v2/scrape")) { return Promise.resolve(firecrawlResponse("firecrawl normalized")) as Promise; } @@ -328,7 +316,7 @@ describe("web_fetch extraction fallbacks", () => { expect(result?.details).toMatchObject({ extractor: "firecrawl" }); const firecrawlCall = fetchSpy.mock.calls.find((call) => - requestUrl(call[0]).includes("/v2/scrape"), + resolveRequestUrl(call[0]).includes("/v2/scrape"), ); expect(firecrawlCall).toBeTruthy(); const init = firecrawlCall?.[1]; @@ -340,7 +328,7 @@ describe("web_fetch extraction fallbacks", () => { installMockFetch( (input: RequestInfo | URL) => Promise.resolve( - htmlResponse("hi", requestUrl(input)), + htmlResponse("hi", resolveRequestUrl(input)), ) as Promise, ); @@ -356,7 +344,7 @@ describe("web_fetch extraction fallbacks", () => { it("throws when readability is empty and firecrawl fails", async () => { installMockFetch((input: RequestInfo | URL) => { - const url = requestUrl(input); + const url = resolveRequestUrl(input); if (url.includes("api.firecrawl.dev")) { return Promise.resolve(firecrawlError()) as Promise; } @@ -373,7 +361,7 @@ describe("web_fetch extraction fallbacks", () => { it("uses firecrawl when direct fetch fails", async () => { installMockFetch((input: RequestInfo | URL) => { - const url = requestUrl(input); + const url = resolveRequestUrl(input); if (url.includes("api.firecrawl.dev")) { return Promise.resolve(firecrawlResponse("firecrawl fallback", url)) as Promise; } @@ -399,7 +387,7 @@ describe("web_fetch extraction fallbacks", () => { const large = "a".repeat(80_000); installMockFetch( (input: RequestInfo | URL) => - Promise.resolve(textResponse(large, requestUrl(input))) as Promise, + Promise.resolve(textResponse(large, resolveRequestUrl(input))) as Promise, ); const tool = createFetchTool({ @@ -427,7 +415,7 @@ describe("web_fetch extraction fallbacks", () => { installMockFetch( (input: RequestInfo | URL) => Promise.resolve( - errorHtmlResponse(html, 404, requestUrl(input), "Text/HTML; charset=utf-8"), + errorHtmlResponse(html, 404, resolveRequestUrl(input), "Text/HTML; charset=utf-8"), ) as Promise, ); @@ -450,7 +438,9 @@ describe("web_fetch extraction fallbacks", () => { "Oops

Oops

"; installMockFetch( (input: RequestInfo | URL) => - Promise.resolve(errorHtmlResponse(html, 500, requestUrl(input), null)) as Promise, + Promise.resolve( + errorHtmlResponse(html, 500, resolveRequestUrl(input), null), + ) as Promise, ); const tool = createFetchTool({ firecrawl: { enabled: false } }); @@ -466,7 +456,7 @@ describe("web_fetch extraction fallbacks", () => { it("wraps firecrawl error details", async () => { installMockFetch((input: RequestInfo | URL) => { - const url = requestUrl(input); + const url = resolveRequestUrl(input); if (url.includes("api.firecrawl.dev")) { return Promise.resolve({ ok: false, diff --git a/src/slack/monitor/media.ts b/src/slack/monitor/media.ts index fca883966a8..a3c8ab5a244 100644 --- a/src/slack/monitor/media.ts +++ b/src/slack/monitor/media.ts @@ -3,6 +3,7 @@ import { normalizeHostname } from "../../infra/net/hostname.js"; import type { FetchLike } from "../../media/fetch.js"; import { fetchRemoteMedia } from "../../media/fetch.js"; import { saveMediaBuffer } from "../../media/store.js"; +import { resolveRequestUrl } from "../../plugin-sdk/request-url.js"; import type { SlackAttachment, SlackFile } from "../types.js"; function isSlackHostname(hostname: string): boolean { @@ -37,23 +38,13 @@ function assertSlackFileUrl(rawUrl: string): URL { return parsed; } -function resolveRequestUrl(input: RequestInfo | URL): string { - if (typeof input === "string") { - return input; - } - if (input instanceof URL) { - return input.toString(); - } - if ("url" in input && typeof input.url === "string") { - return input.url; - } - throw new Error("Unsupported fetch input: expected string, URL, or Request"); -} - function createSlackMediaFetch(token: string): FetchLike { let includeAuth = true; return async (input, init) => { const url = resolveRequestUrl(input); + if (!url) { + throw new Error("Unsupported fetch input: expected string, URL, or Request"); + } const { headers: initHeaders, redirect: _redirect, ...rest } = init ?? {}; const headers = new Headers(initHeaders);