fix(web-fetch): handle network-level fetch errors in fetchFirecrawlContent
When fetch() fails at the network level (timeout, connection refused, etc.), the error was not being caught within fetchFirecrawlContent, causing unhandled promise rejections that could crash the gateway. This change wraps the fetch() and res.json() calls in try-catch blocks to: - Catch network-level fetch failures and throw a descriptive error - Catch invalid JSON response errors and throw a descriptive error The descriptive errors propagate up to the tool execution wrapper which converts them to proper tool error results. Fixes #4392 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
da71eaebd2
commit
fce6e08594
@ -282,17 +282,23 @@ export async function fetchFirecrawlContent(params: {
|
|||||||
storeInCache: params.storeInCache,
|
storeInCache: params.storeInCache,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
let res: Response;
|
||||||
method: "POST",
|
try {
|
||||||
headers: {
|
res = await fetch(endpoint, {
|
||||||
Authorization: `Bearer ${params.apiKey}`,
|
method: "POST",
|
||||||
"Content-Type": "application/json",
|
headers: {
|
||||||
},
|
Authorization: `Bearer ${params.apiKey}`,
|
||||||
body: JSON.stringify(body),
|
"Content-Type": "application/json",
|
||||||
signal: withTimeout(undefined, params.timeoutSeconds * 1000),
|
},
|
||||||
});
|
body: JSON.stringify(body),
|
||||||
|
signal: withTimeout(undefined, params.timeoutSeconds * 1000),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`Firecrawl fetch failed (network error): ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = (await res.json()) as {
|
let payload: {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
data?: {
|
data?: {
|
||||||
markdown?: string;
|
markdown?: string;
|
||||||
@ -306,6 +312,12 @@ export async function fetchFirecrawlContent(params: {
|
|||||||
warning?: string;
|
warning?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
try {
|
||||||
|
payload = (await res.json()) as typeof payload;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`Firecrawl fetch failed (invalid response): ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok || payload?.success === false) {
|
if (!res.ok || payload?.success === false) {
|
||||||
const detail = payload?.error || res.statusText;
|
const detail = payload?.error || res.statusText;
|
||||||
|
|||||||
@ -269,4 +269,75 @@ describe("web_fetch extraction fallbacks", () => {
|
|||||||
/Web fetch failed \(500\):.*Oops/,
|
/Web fetch failed \(500\):.*Oops/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles network-level fetch failure in firecrawl fallback", async () => {
|
||||||
|
const mockFetch = vi.fn((input: RequestInfo) => {
|
||||||
|
const url = requestUrl(input);
|
||||||
|
if (url.includes("api.firecrawl.dev")) {
|
||||||
|
return Promise.reject(new TypeError("fetch failed"));
|
||||||
|
}
|
||||||
|
// Direct fetch fails with 403
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
headers: makeHeaders({ "content-type": "text/html" }),
|
||||||
|
text: async () => "blocked",
|
||||||
|
} as Response);
|
||||||
|
});
|
||||||
|
// @ts-expect-error mock fetch
|
||||||
|
global.fetch = mockFetch;
|
||||||
|
|
||||||
|
const tool = createWebFetchTool({
|
||||||
|
config: {
|
||||||
|
tools: {
|
||||||
|
web: {
|
||||||
|
fetch: { cacheTtlMinutes: 0, firecrawl: { apiKey: "firecrawl-test" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sandboxed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tool?.execute?.("call", { url: "https://example.com/network-error" }),
|
||||||
|
).rejects.toThrow(/Firecrawl fetch failed \(network error\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles invalid JSON response from firecrawl", async () => {
|
||||||
|
const mockFetch = vi.fn((input: RequestInfo) => {
|
||||||
|
const url = requestUrl(input);
|
||||||
|
if (url.includes("api.firecrawl.dev")) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => {
|
||||||
|
throw new SyntaxError("Unexpected token");
|
||||||
|
},
|
||||||
|
} as Response);
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
headers: makeHeaders({ "content-type": "text/html" }),
|
||||||
|
text: async () => "blocked",
|
||||||
|
} as Response);
|
||||||
|
});
|
||||||
|
// @ts-expect-error mock fetch
|
||||||
|
global.fetch = mockFetch;
|
||||||
|
|
||||||
|
const tool = createWebFetchTool({
|
||||||
|
config: {
|
||||||
|
tools: {
|
||||||
|
web: {
|
||||||
|
fetch: { cacheTtlMinutes: 0, firecrawl: { apiKey: "firecrawl-test" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sandboxed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tool?.execute?.("call", { url: "https://example.com/invalid-json" }),
|
||||||
|
).rejects.toThrow(/Firecrawl fetch failed \(invalid response\)/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user