heygen-com/hyperframes · error

Failed to download ${url}: HTTP ${res.status} ${res.statusTe

Error message

Failed to download ${url}: HTTP ${res.status} ${res.statusText}

What it means

Thrown by downloadToFile when the fetch resolves with res.ok === false. The message surfaces the URL, HTTP status, and statusText verbatim so the caller can tell an expired presigned URL (403), a missing object (404), or a server-side failure (5xx) apart. It fires before any file is opened, so no partial artifact is created at destPath.

Source

Thrown at packages/cli/src/cloud/download.ts:47

  bytes: number;
}

/**
 * Stream `url` into `destPath`. Creates the parent directory if needed,
 * truncates any existing file at the destination, and deletes the
 * partial output on any error so the caller never observes a corrupt
 * file at the returned path.
 */
// fallow-ignore-next-line complexity
export async function downloadToFile(
  url: string,
  destPath: string,
  options: DownloadOptions = {},
): Promise<DownloadResult> {
  const fetchImpl = options.fetchImpl ?? fetch;
  const res = await fetchImpl(url, { signal: options.signal });
  if (!res.ok) {
    throw new Error(`Failed to download ${url}: HTTP ${res.status} ${res.statusText}`);
  }
  if (!res.body) {
    throw new Error(`Failed to download ${url}: empty response body`);
  }

  mkdirSync(dirname(destPath), { recursive: true });

  const totalHeader = res.headers.get("content-length");
  const total = totalHeader ? Number.parseInt(totalHeader, 10) : undefined;
  const totalOpt = total !== undefined && Number.isFinite(total) ? total : undefined;

  const file = createWriteStream(destPath);
  let bytes = 0;
  let errored = false;
  try {
    for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) {
      if (options.signal?.aborted) {
        throw options.signal.reason instanceof Error

View on GitHub (pinned to c2996c8626)

Solutions

  1. Refetch a fresh asset/URL via `hyperframes cloud get` (or the equivalent reserve call) and retry — most cases are expired presigned URLs.
  2. Confirm the object still exists in the cloud (status 404 means it is gone; you must re-create or re-upload it).
  3. For 5xx, retry with backoff a couple of times before treating as a hard failure.
  4. If 401/403 persists on a freshly issued URL, check credentials and clock skew on the signing host.

Example fix

// before: a possibly-stale URL reused across retries
await downloadToFile(staleUrl, dest);

// after: refresh the presigned URL when the download 403s
try {
  await downloadToFile(url, dest);
} catch (err) {
  if (/HTTP 403/.test(String(err?.message))) {
    const fresh = await client.getAssetUrl(id);
    await downloadToFile(fresh, dest);
  } else throw err;
}
Defensive patterns

Strategy: retry

Validate before calling

async function assertDownloadable(url: string): Promise<void> {
  const res = await fetch(url, { method: 'GET' });
  if (!res.ok) throw new Error(`preflight: HTTP ${res.status}`);
  res.body?.cancel();
}

Try / catch

async function downloadWithRefresh(getUrl: () => Promise<string>, dest: string) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      await downloadToFile(await getUrl(), dest);
      return;
    } catch (err) {
      if (/HTTP 40[03]/.test(String(err?.message)) && attempt < 2) continue;
      throw err;
    }
  }
}

Prevention

When it happens

Trigger: Downloading a presigned S3/GCS URL after its expiry window (403 Forbidden), fetching an asset whose object was deleted between manifest generation and download (404), a 5xx from the object store, or any redirect/auth failure that returns non-2xx.

Common situations: A presigned URL sits idle past its TTL (common when the asset was reserved, then the download was queued behind other work); the cloud asset was garbage-collected; a network appliance rewrites the response to a 401 captive-portal page.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/a51e71ba70717719. Report an issue: GitHub.