heygen-com/hyperframes · error

Direct upload PUT failed: ${res.status} ${res.statusText}${d

Error message

Direct upload PUT failed: ${res.status} ${res.statusText}${detail ? ` — ${detail.slice(0, 300)}` : ""}

What it means

Thrown when the S3/GCS direct-upload PUT (the second leg of the multipart asset upload) returns a non-2xx response. The message includes the HTTP status, statusText, and up to 300 chars of the response body for diagnosis. It fires inside the PUT step itself; the separate completeWithRetry step handles the finalize race and is not where this error originates.

Source

Thrown at packages/cli/src/cloud/upload.ts:103

  uploadUrl: string,
  uploadHeaders: Record<string, unknown>,
  bytes: Uint8Array,
): Promise<void> {
  const headers: Record<string, string> = {
    "content-type": CONTENT_TYPE_ZIP,
    ...normalizeUploadHeaders(uploadHeaders),
  };
  // `Uint8Array<ArrayBufferLike>` is a valid `BodyInit` at runtime but
  // not strictly assignable per lib.dom.d.ts — cast rather than copy,
  // since a 200MB buffer copy would be wasteful.
  const res = await fetchImpl(uploadUrl, {
    method: "PUT",
    headers,
    body: bytes as unknown as BodyInit,
  });
  if (!res.ok) {
    const detail = await res.text().catch(() => "");
    throw new Error(
      `Direct upload PUT failed: ${res.status} ${res.statusText}${
        detail ? ` — ${detail.slice(0, 300)}` : ""
      }`,
    );
  }
}

// Complete with retry-on-409. Retry ONLY on the documented "PUT not
// visible yet" race between S3 write consistency and finalize; any other
// error surfaces immediately. `completeAssetUpload` itself is idempotent,
// so retrying an already-succeeded call is safe.
async function completeWithRetry(
  client: HyperframesCloudClient,
  asset_id: string,
  checksum_sha256: string,
): Promise<{ asset_id: string }> {
  let lastErr: unknown;
  for (let attempt = 0; attempt < COMPLETE_MAX_RETRIES; attempt++) {

View on GitHub (pinned to c2996c8626)

Solutions

  1. Re-reserve the upload (call the asset-create/initiate endpoint again) to get a fresh presigned URL, then retry the PUT immediately.
  2. Strip any custom uploadHeaders that were not part of the signed header set, or have the server sign the headers you need.
  3. Verify host clock skew (AWS SigV4 rejects requests more than a few minutes off).
  4. If the asset is large, switch to multipart upload rather than a single PUT.

Example fix

// before: custom header breaks the signature
await directUploadPut(url, bytes, { 'Content-Encoding': 'br' });

// after: only send headers the presigned URL signed, and refresh on 403
try {
  await directUploadPut(url, bytes);
} catch (err) {
  if (/PUT failed: 403/.test(String(err?.message))) {
    const fresh = await reserveUpload(assetId);
    await directUploadPut(fresh.url, bytes);
  } else throw err;
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: ensure headers you send are part of the signed set
function assertSignedHeaders(uploadUrl: URL, headers: Record<string,string>) {
  // presigned URLs typically encode signed headers in the querystring
  const signed = new Set(uploadUrl.searchParams.keys());
  for (const h of Object.keys(headers)) {
    if (!signed.has(h.toLowerCase())) throw new Error(`header ${h} not in signed set`);
  }
}

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    await directUploadPut(uploadUrl, bytes);
    break;
  } catch (err) {
    if (/PUT failed: 40[03]/.test(String(err?.message)) && attempt < 2) {
      uploadUrl = (await reserveUpload(id)).url;  // refresh
      continue;
    }
    throw err;
  }
}

Prevention

When it happens

Trigger: The presigned PUT URL expired before the bytes were sent (403), a signature/headers mismatch caused by overriding Content-Type or adding headers not in the signed set, the object exceeds a bucket size policy, or the storage backend returned 5xx.

Common situations: Long delay between reserving the upload and issuing the PUT; injecting custom uploadHeaders that break the signature (e.g. adding Content-Encoding the presigned URL did not sign); uploading a >5GB single PUT where multipart was required; clock skew on the signing client.

Related errors


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