vercel/ai · error · DownloadError

Redirect from ${currentUrl} could not be validated and was b

Error message

Redirect from ${currentUrl} could not be validated and was blocked

What it means

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.

Source

Thrown at packages/provider-utils/src/fetch-with-validated-redirects.ts:116

  for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
    // The developer-configured origin is trusted by definition; validating it
    // would reject legitimate self-hosted / localhost deployments.
    const isTrustedHop =
      trustedOrigin !== undefined && isSameOrigin(currentUrl, trustedOrigin);

    if (!isTrustedHop) {
      validateDownloadUrl(currentUrl);
    }

    const fetch =
      customFetch ??
      (isTrustedHop ? globalThis.fetch : await getDefaultDownloadFetch());

    const response = await fetch(currentUrl, perHopInit('manual'));

    if (response.type === 'opaqueredirect') {
      if (!isBrowserRuntime()) {
        throw new DownloadError({
          url,
          message: `Redirect from ${currentUrl} could not be validated and was blocked`,
        });
      }
      return await fetch(currentUrl, perHopInit('follow'));
    }

    const location = response.headers.get('location');
    if (REDIRECT_STATUS_CODES.has(response.status) && location) {
      // Release the redirect response's connection before moving to the next
      // hop. Whether that hop is followed or rejected by the guard, an
      // unconsumed 3xx body would leak the underlying socket.
      await cancelResponseBody(response);
      const nextUrl = new URL(location, currentUrl).toString();

      // Drop all caller headers except the user-agent before following a
      // redirect that crosses origin. Only stripping Authorization (as the
      // fetch spec does) is not enough on the server: providers authenticate

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Resolve the redirect yourself: fetch the URL and read the Location header, then pass the final direct URL to the SDK.
  2. Use a download host that serves assets directly (200) instead of redirecting, e.g. the CDN's canonical URL.
  3. 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.
  4. Check the currentUrl in the error message — if it points to a private/internal address, the block is intentional SSRF protection.

Example fix

// before: passing a redirecting URL
await generateImage({ model, prompt, ... }); // provider file URL redirects

// after: resolve redirects first, pass the final URL
const res = await fetch(redirectingUrl, { redirect: 'follow' });
const finalUrl = res.url; // actual served URL
// pass finalUrl (or the bytes as a data URL) instead of the redirecting URL
Defensive patterns

Strategy: try-catch

Validate before calling

async function assertNoOpaqueRedirect(url: string): Promise<void> {
  const res = await fetch(url, { redirect: 'manual' });
  if (res.type === 'opaqueredirect') {
    throw new Error(`URL redirects opaquely and will be blocked on server runtimes: ${url}`);
  }
}

Type guard

import { DownloadError } from '@ai-sdk/provider-utils';
function isRedirectBlocked(e: unknown): boolean {
  return DownloadError.isInstance(e) && e.message.includes('could not be validated and was blocked');
}

Try / catch

try {
  await downloadBlob(url);
} catch (error) {
  if (DownloadError.isInstance(error) && error.message.includes('could not be validated')) {
    // resolve the redirect chain yourself and pass the final URL, or fetch bytes and pass a data URL
  }
  throw error;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/8d856140f36ad6aa. Report an issue: GitHub.