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 authenticateView on GitHub (pinned to 69428b1f8b)
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.
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
- 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.
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
- OAuth endpoint URL is not allowed: ${endpointUrl.href}
- Too many redirects (max ${maxRedirects})
- Tool approval signature verification failed for approval "${
- Video generation timed out after ${timeoutMs}ms.
- The response body is empty.
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/8d856140f36ad6aa.
Report an issue: GitHub.