{"record":{"id":"8d856140f36ad6aa","repo":"vercel/ai","slug":"redirect-from-currenturl-could-not-be-validated","errorCode":null,"errorMessage":"Redirect from ${currentUrl} could not be validated and was blocked","messagePattern":"Redirect from (.+?) could not be validated and was blocked","errorType":"exception","errorClass":"DownloadError","httpStatus":null,"severity":"error","filePath":"packages/provider-utils/src/fetch-with-validated-redirects.ts","lineNumber":116,"sourceCode":"  for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {\n    // The developer-configured origin is trusted by definition; validating it\n    // would reject legitimate self-hosted / localhost deployments.\n    const isTrustedHop =\n      trustedOrigin !== undefined && isSameOrigin(currentUrl, trustedOrigin);\n\n    if (!isTrustedHop) {\n      validateDownloadUrl(currentUrl);\n    }\n\n    const fetch =\n      customFetch ??\n      (isTrustedHop ? globalThis.fetch : await getDefaultDownloadFetch());\n\n    const response = await fetch(currentUrl, perHopInit('manual'));\n\n    if (response.type === 'opaqueredirect') {\n      if (!isBrowserRuntime()) {\n        throw new DownloadError({\n          url,\n          message: `Redirect from ${currentUrl} could not be validated and was blocked`,\n        });\n      }\n      return await fetch(currentUrl, perHopInit('follow'));\n    }\n\n    const location = response.headers.get('location');\n    if (REDIRECT_STATUS_CODES.has(response.status) && location) {\n      // Release the redirect response's connection before moving to the next\n      // hop. Whether that hop is followed or rejected by the guard, an\n      // unconsumed 3xx body would leak the underlying socket.\n      await cancelResponseBody(response);\n      const nextUrl = new URL(location, currentUrl).toString();\n\n      // Drop all caller headers except the user-agent before following a\n      // redirect that crosses origin. Only stripping Authorization (as the\n      // fetch spec does) is not enough on the server: providers authenticate","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/provider-utils/src/fetch-with-validated-redirects.ts#L98-L134","documentation":"To protect against SSRF, fetchWithValidatedRedirects follows redirects manually and validates every hop's URL before requesting it. When a redirect comes back as an opaque response (redirect: 'manual'), its target cannot be read or validated. In non-browser runtimes the library fails closed and blocks the redirect rather than following it unvalidated. In browsers it is safe to re-fetch with redirect: 'follow' because CORS constrains where the request can land.","triggerScenarios":"A downloaded URL (provider-returned or user-supplied file URL) responds with a redirect that surfaces as response.type === 'opaqueredirect' in Node/edge runtimes — typically a cross-origin 30x whose target cannot be validated, or a server issuing redirects in a way that yields opaque responses (e.g. no-cors mode requests).","commonSituations":"Asset hosts behind redirect chains to CDNs on other origins; internal short-linkers redirecting to private addresses (deliberately blocked as SSRF); running in Cloudflare Workers/Node where opaque redirects are unreadable; misconfigured storage returning 307/308 to unresolvable hosts.","solutions":["Resolve the redirect yourself: fetch the URL and read the Location header, then pass the final direct URL to the SDK.","Use a download host that serves assets directly (200) instead of redirecting, e.g. the CDN's canonical URL.","If the redirect target is legitimate and public, whitelist/allowlist that origin in your proxy layer or fetch it upstream and hand the bytes to the SDK as a file/data URL.","Check the currentUrl in the error message — if it points to a private/internal address, the block is intentional SSRF protection."],"exampleFix":"// before: passing a redirecting URL\nawait generateImage({ model, prompt, ... }); // provider file URL redirects\n\n// after: resolve redirects first, pass the final URL\nconst res = await fetch(redirectingUrl, { redirect: 'follow' });\nconst finalUrl = res.url; // actual served URL\n// pass finalUrl (or the bytes as a data URL) instead of the redirecting URL","handlingStrategy":"try-catch","validationCode":"async function assertNoOpaqueRedirect(url: string): Promise<void> {\n  const res = await fetch(url, { redirect: 'manual' });\n  if (res.type === 'opaqueredirect') {\n    throw new Error(`URL redirects opaquely and will be blocked on server runtimes: ${url}`);\n  }\n}","typeGuard":"import { DownloadError } from '@ai-sdk/provider-utils';\nfunction isRedirectBlocked(e: unknown): boolean {\n  return DownloadError.isInstance(e) && e.message.includes('could not be validated and was blocked');\n}","tryCatchPattern":"try {\n  await downloadBlob(url);\n} catch (error) {\n  if (DownloadError.isInstance(error) && error.message.includes('could not be validated')) {\n    // resolve the redirect chain yourself and pass the final URL, or fetch bytes and pass a data URL\n  }\n  throw error;\n}","preventionTips":["Serve media from direct 200-responding URLs, not redirecting short links.","Resolve redirect chains client-side and pass the final URL to the SDK.","If the error's URL points at a private/internal address, the block is intentional SSRF protection — do not bypass it.","In server runtimes, fetch cross-origin redirecting assets upstream and hand bytes to the SDK instead."],"tags":["security","ssrf","redirect","network"],"backgroundTag":"redirect-blocked","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}