heygen-com/hyperframes · error

Failed to download ${url}: empty response body

Error message

Failed to download ${url}: empty response body

What it means

Thrown by downloadToFile when the response is res.ok (2xx) but res.body is null/undefined. This is an unusual server behavior — a success status with no body — that the client refuses to silently turn into a zero-byte file. Like the HTTP-error branch it fires before destPath is created, so no partial artifact appears.

Source

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

/**
 * 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
          ? options.signal.reason
          : new Error("Download aborted");
      }

View on GitHub (pinned to c2996c8626)

Solutions

  1. Verify the URL actually points at a non-empty object (curl -i and check Content-Length).
  2. If the object genuinely is empty, handle the empty case explicitly upstream rather than relying on downloadToFile.
  3. For test doubles, ensure the mocked Response includes a non-empty ReadableStream body.
  4. Re-reserve the asset URL if the store returned a degenerate response for a known-nonempty object.

Example fix

// before: test mock returns ok but no body
fetchMock.mockResponse('', { status: 200 });

// after: include a body stream
fetchMock.mockResponse(Buffer.from(bytes), { status: 200 });
Defensive patterns

Strategy: validation

Validate before calling

async function assertNonEmptyBody(url: string): Promise<void> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  if (!res.body) throw new Error('server returned 2xx with no body — empty or misconfigured object');
}

Try / catch

try {
  await downloadToFile(url, dest);
} catch (err) {
  if (/empty response body/.test(String(err?.message))) {
    // treat as a degenerate object; re-reserve or skip
  } else throw err;
}

Prevention

When it happens

Trigger: A server returns 200 with Content-Length: 0 and no body, or a fetch implementation/runtime where the stream is absent (some non-browser runtimes, mocked fetches in tests, or a HEAD-shaped response to a GET).

Common situations: A misconfigured object store or CDN returning an empty 200 for a zero-size object; a test double that returns { ok: true } without a body; a streaming transport that closed the connection before emitting any chunk.

Related errors


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