vercel/ai · error · DownloadError

Failed to download ${url}: ${statusCode} ${statusText}

Error message

Failed to download ${url}: ${statusCode} ${statusText}

What it means

downloadBlob (used internally when the SDK fetches media files from URLs, e.g. provider-returned image/video URLs) wraps every non-ok HTTP response in a DownloadError carrying the status code and status text. The library cancels the body first to avoid leaking sockets, then throws so callers get a consistent error type for failed downloads.

Source

Thrown at packages/provider-utils/src/download-blob.ts:34

 * @returns A Promise that resolves to the downloaded Blob.
 *
 * @throws DownloadError if the download fails or exceeds maxBytes.
 */
export async function downloadBlob(
  url: string,
  options?: { maxBytes?: number; abortSignal?: AbortSignal },
): Promise<Blob> {
  try {
    const response = await fetchWithValidatedRedirects({
      url,
      abortSignal: options?.abortSignal,
    });

    if (!response.ok) {
      // Release the connection before rejecting so an error status from an
      // attacker-controlled origin cannot leak open sockets.
      await cancelResponseBody(response);
      throw new DownloadError({
        url,
        statusCode: response.status,
        statusText: response.statusText,
      });
    }

    const data = await readResponseWithSizeLimit({
      response,
      url,
      maxBytes: options?.maxBytes ?? DEFAULT_MAX_DOWNLOAD_SIZE,
    });

    const contentType = response.headers.get('content-type') ?? undefined;
    return new Blob([data], contentType ? { type: contentType } : undefined);
  } catch (error) {
    if (DownloadError.isInstance(error)) {
      throw error;
    }

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Check the statusCode/statusText in the DownloadError: 403/401 means credentials, 404/410 means the URL expired or is gone.
  2. If the provider returns expiring URLs, download the asset promptly after the response or re-request a fresh URL.
  3. Retry with backoff for 429/5xx statuses; fail fast for 4xx client errors.
  4. Verify the URL is reachable (curl -I) and that any required auth headers are supplied via the fetch option where supported.

Example fix

// before: assuming every URL works
const blob = await downloadBlob(url);

// after: handle status-based failures
try {
  const blob = await downloadBlob(url, { abortSignal });
} catch (e) {
  if (DownloadError.isInstance(e) && e.statusCode === 403) {
    url = await refreshSignedUrl(); // re-fetch expired URL
    return downloadBlob(url);
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

async function assertUrlReachable(url: string): Promise<void> {
  const res = await fetch(url, { method: 'HEAD' });
  if (!res.ok) throw new Error(`URL not downloadable: ${res.status} ${res.statusText}`);
}

Type guard

import { DownloadError } from '@ai-sdk/provider-utils';
function isDownloadError(e: unknown): e is DownloadError {
  return DownloadError.isInstance(e);
}

Try / catch

try {
  const blob = await downloadBlob(url);
} catch (error) {
  if (DownloadError.isInstance(error)) {
    if (error.statusCode === 404 || error.statusCode === 410) {
      // request a fresh asset URL from the provider
    } else if (error.statusCode === 429 || error.statusCode >= 500) {
      // retry with backoff
    }
  }
  throw error;
}

Prevention

When it happens

Trigger: A file URL passed to the SDK (or a URL returned by a provider in a response) was fetched and the server replied with a non-2xx status — 404 for a deleted/expired asset, 403 for a URL requiring auth, 410 for expired signed URLs, 5xx for origin problems.

Common situations: Expired pre-signed S3/CDN URLs returned by a provider; private URLs fetched without credentials; typo'd baseURL or asset path; rate limiting (429) or temporary origin outages (502/503).

Related errors


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