heygen-com/hyperframes · error

Truncated download: got ${bytes} bytes, expected ${totalOpt}

Error message

Truncated download: got ${bytes} bytes, expected ${totalOpt} (content-length). The presigned URL may have expired mid-transfer — refetch via `hyperframes cloud get`.

What it means

Thrown after the download stream completes when the accumulated byte count does not equal the content-length the server advertised. The message points at the most common cause — a presigned URL expiring mid-transfer — and the finally block unlinks the partial file so a corrupt artifact can never be observed at destPath. totalOpt is only set when content-length was present and finite, so responses without it cannot trip this check.

Source

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

  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
          ? options.signal.reason
          : new Error("Download aborted");
      }
      bytes += chunk.byteLength;
      options.onProgress?.(bytes, totalOpt);
      if (!file.write(chunk)) {
        await waitForDrain(file, options.signal);
      }
    }
    if (totalOpt !== undefined && bytes !== totalOpt) {
      throw new Error(
        `Truncated download: got ${bytes} bytes, expected ${totalOpt} (content-length). ` +
          `The presigned URL may have expired mid-transfer — refetch via \`hyperframes cloud get\`.`,
      );
    }
  } catch (err) {
    errored = true;
    throw err;
  } finally {
    await closeFile(file);
    if (errored) {
      // Don't let a partial file pose as the final artifact. Best-
      // effort unlink — if it fails (already gone, permission), we
      // re-throw the original error.
      try {
        unlinkSync(destPath);
      } catch {
        /* swallow */
      }

View on GitHub (pinned to c2996c8626)

Solutions

  1. Refetch a fresh presigned URL via `hyperframes cloud get` and retry the download from scratch.
  2. For very large assets, request shorter-TTL-bounded chunks or raise the presigned-URL lifetime with the API owner.
  3. Stabilize the network (wired CI runner, larger TCP buffers) if the cause is a mid-stream drop rather than expiry.
  4. Wrap downloadToFile in a bounded retry loop keyed on the 'Truncated download' message.

Example fix

let lastErr: unknown;
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    const url = await client.getAssetUrl(assetId); // fresh each attempt
    await downloadToFile(url, dest, { signal });
    return;
  } catch (err) {
    lastErr = err;
    if (!/Truncated download/.test(String(err?.message))) throw err;
  }
}
throw lastErr;
Defensive patterns

Strategy: retry

Validate before calling

// Cannot fully prevent mid-stream truncation, but bound the window:
// reserve the URL right before download and keep total duration < TTL.

Type guard

function isPollTimeoutLike(err: unknown): boolean {
  return err instanceof Error && /Truncated download/.test(err.message);
}

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    const url = await client.getAssetUrl(id);
    await downloadToFile(url, dest);
    break;
  } catch (err) {
    if (!/Truncated download/.test(String(err?.message)) || attempt === 2) throw err;
  }
}

Prevention

When it happens

Trigger: A presigned URL expires while bytes are still flowing (S3 returns 403 mid-stream or truncates), a network drop closes the socket early without an error event, or a proxy enforces a smaller max-body-size than content-length and silently truncates.

Common situations: Large multi-hundred-MB uploads/downloads that outlast the presigned-URL TTL; flaky mobile/CI networks; a CDN with a response size cap; downloads started near the end of the URL's validity window.

Related errors


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