{"record":{"id":"86105b0b50b928b4","repo":"unionlabs/union","slug":"asset-asset-id-size-mismatch-expected-asset","errorCode":null,"errorMessage":"Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength}","messagePattern":"Asset (.+?) size mismatch: expected (.+?), received (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"site/scripts/import-contentful-export.mjs","lineNumber":177,"sourceCode":"  await mkdir(join(target, \"..\"), { recursive: true })\n\n  let existingSize = null\n  try {\n    existingSize = (await stat(target)).size\n  } catch {\n    // The file has not been downloaded yet.\n  }\n\n  if (existingSize !== asset.size) {\n    const response = await fetch(asset.sourceUrl)\n    if (!response.ok) {\n      throw new Error(\n        `Failed to download asset ${asset.id}: ${response.status} ${response.statusText}`,\n      )\n    }\n    const bytes = new Uint8Array(await response.arrayBuffer())\n    if (asset.size !== null && bytes.byteLength !== asset.size) {\n      throw new Error(\n        `Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength}`,\n      )\n    }\n    await writeFile(target, bytes)\n  }\n\n  completedAssets += 1\n  if (\n    completedAssets % 25 === 0\n    || completedAssets === downloadableAssets.length\n  ) {\n    console.log(`assets: ${completedAssets}/${downloadableAssets.length}`)\n  }\n})\n\nconst assetById = new Map(assetManifest.map((asset) => [asset.id, asset]))\n\nfunction assetFromLink(value) {","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/unionlabs/union/blob/031785bb6dc6b957c624e62bc64c184409c97d7b/site/scripts/import-contentful-export.mjs#L159-L195","documentation":"Thrown by the site's Contentful import script while syncing assets. The script re-downloads an asset whenever the file already on disk differs from `file.details.size` recorded in the export (see localAsset(), size: file.details?.size ?? null), and treats any difference between the manifest size and the downloaded byte count as a corrupted or wrong-version download, aborting before writing. It is an integrity guard: the manifest and the CDN must agree.","triggerScenarios":"Running import-contentful-export.mjs when the bytes currently served at `https:<file.url>` differ from the export's recorded size: asset re-uploaded or re-processed on Contentful after the export was taken, export taken from a different space/environment than the URLs point at, a transforming proxy or redirect returning different content with a 200 status, or a previous partial write leaving a stale on-disk size that forces a re-download that now disagrees with a stale manifest.","commonSituations":"Stale export JSON paired with the live ctfassets CDN; wrong previewDirectory/deliveryDirectory arguments; an upstream asset replaced between export and import; corporate proxies or auth walls serving an HTML page with status 200; rerunning the import after an interrupted first run.","solutions":["Re-run the Contentful export so assets.json metadata matches what the CDN currently serves, then re-run the import","Spot-check the failing asset: curl -sL -o /dev/null -w '%{size_download}' <asset.sourceUrl> and compare with the value in site/content/archive/contentful/asset-manifest.json","Delete the cached file at site/public<asset.path> (a partial earlier write) and re-run","Confirm both script arguments point at exports of the same space/environment the asset URLs belong to","Do not disable the check by nulling asset.size; the mismatch means manifest and source genuinely disagree"],"exampleFix":"// before\nconst bytes = new Uint8Array(await response.arrayBuffer())\nif (asset.size !== null && bytes.byteLength !== asset.size) {\n  throw new Error(`Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength}`)\n}\n\n// after — fail fast on the server-declared length and include the URL for diagnosis\nconst declared = Number(response.headers.get(\"content-length\"))\nif (asset.size !== null && Number.isFinite(declared) && declared !== asset.size) {\n  throw new Error(`Asset ${asset.id} mismatch pre-download: manifest=${asset.size} cdn=${declared} (${asset.sourceUrl}) — re-export from Contentful`)\n}\nconst bytes = new Uint8Array(await response.arrayBuffer())\nif (asset.size !== null && bytes.byteLength !== asset.size) {\n  throw new Error(`Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength} (${asset.sourceUrl})`)\n}","handlingStrategy":"validation","validationCode":"// Pre-flight: compare manifest sizes against the CDN before importing\nimport { readFile } from \"node:fs/promises\"\nconst manifest = JSON.parse(await readFile(\"site/content/archive/contentful/asset-manifest.json\", \"utf8\"))\nconst stale = []\nfor (const asset of manifest.filter((a) => a.sourceUrl)) {\n  const head = await fetch(asset.sourceUrl, { method: \"HEAD\" })\n  const len = Number(head.headers.get(\"content-length\"))\n  if (asset.size !== null && head.ok && Number.isFinite(len) && len !== asset.size) {\n    stale.push(`${asset.id}: manifest=${asset.size} cdn=${len}`)\n  }\n}\nif (stale.length) throw new Error(`Stale export, re-export from Contentful:\\n${stale.join(\"\\n\")}`)","typeGuard":"const hasReliableSize = (asset) => asset.size === null || (Number.isInteger(asset.size) && asset.size >= 0)","tryCatchPattern":"const failures = []\nawait mapConcurrent(downloadableAssets, 8, async (asset) => {\n  try {\n    await downloadAsset(asset)\n  } catch (error) {\n    failures.push({ id: asset.id, url: asset.sourceUrl, message: String(error) })\n  }\n})\nif (failures.length) {\n  throw new Error(`asset import failures:\\n${failures.map((f) => `${f.id}: ${f.message}`).join(\"\\n\")}`)\n}","preventionTips":["Always regenerate the Contentful export immediately before running the import so manifest and CDN agree","Run imports in an environment without transforming proxies (no auth walls, no content rewriting)","Keep one export directory per space/environment and label arguments explicitly","Treat a size mismatch as a staleness signal — re-export instead of bypassing the check"],"tags":["contentful","content-import","download-integrity","cdn","javascript"],"backgroundTag":null,"analyzedSha":"031785bb6dc6b957c624e62bc64c184409c97d7b","analyzedAt":"2026-08-16T06:24:09.996Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}