unionlabs/union · error · Error

Asset ${asset.id} size mismatch: expected ${asset.size}, rec

Error message

Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength}

What it means

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.

Source

Thrown at site/scripts/import-contentful-export.mjs:177

  await mkdir(join(target, ".."), { recursive: true })

  let existingSize = null
  try {
    existingSize = (await stat(target)).size
  } catch {
    // The file has not been downloaded yet.
  }

  if (existingSize !== asset.size) {
    const response = await fetch(asset.sourceUrl)
    if (!response.ok) {
      throw new Error(
        `Failed to download asset ${asset.id}: ${response.status} ${response.statusText}`,
      )
    }
    const bytes = new Uint8Array(await response.arrayBuffer())
    if (asset.size !== null && bytes.byteLength !== asset.size) {
      throw new Error(
        `Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength}`,
      )
    }
    await writeFile(target, bytes)
  }

  completedAssets += 1
  if (
    completedAssets % 25 === 0
    || completedAssets === downloadableAssets.length
  ) {
    console.log(`assets: ${completedAssets}/${downloadableAssets.length}`)
  }
})

const assetById = new Map(assetManifest.map((asset) => [asset.id, asset]))

function assetFromLink(value) {

View on GitHub (pinned to 031785bb6d)

Solutions

  1. Re-run the Contentful export so assets.json metadata matches what the CDN currently serves, then re-run the import
  2. 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
  3. Delete the cached file at site/public<asset.path> (a partial earlier write) and re-run
  4. Confirm both script arguments point at exports of the same space/environment the asset URLs belong to
  5. Do not disable the check by nulling asset.size; the mismatch means manifest and source genuinely disagree

Example fix

// before
const bytes = new Uint8Array(await response.arrayBuffer())
if (asset.size !== null && bytes.byteLength !== asset.size) {
  throw new Error(`Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength}`)
}

// after — fail fast on the server-declared length and include the URL for diagnosis
const declared = Number(response.headers.get("content-length"))
if (asset.size !== null && Number.isFinite(declared) && declared !== asset.size) {
  throw new Error(`Asset ${asset.id} mismatch pre-download: manifest=${asset.size} cdn=${declared} (${asset.sourceUrl}) — re-export from Contentful`)
}
const bytes = new Uint8Array(await response.arrayBuffer())
if (asset.size !== null && bytes.byteLength !== asset.size) {
  throw new Error(`Asset ${asset.id} size mismatch: expected ${asset.size}, received ${bytes.byteLength} (${asset.sourceUrl})`)
}
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: compare manifest sizes against the CDN before importing
import { readFile } from "node:fs/promises"
const manifest = JSON.parse(await readFile("site/content/archive/contentful/asset-manifest.json", "utf8"))
const stale = []
for (const asset of manifest.filter((a) => a.sourceUrl)) {
  const head = await fetch(asset.sourceUrl, { method: "HEAD" })
  const len = Number(head.headers.get("content-length"))
  if (asset.size !== null && head.ok && Number.isFinite(len) && len !== asset.size) {
    stale.push(`${asset.id}: manifest=${asset.size} cdn=${len}`)
  }
}
if (stale.length) throw new Error(`Stale export, re-export from Contentful:\n${stale.join("\n")}`)

Type guard

const hasReliableSize = (asset) => asset.size === null || (Number.isInteger(asset.size) && asset.size >= 0)

Try / catch

const failures = []
await mapConcurrent(downloadableAssets, 8, async (asset) => {
  try {
    await downloadAsset(asset)
  } catch (error) {
    failures.push({ id: asset.id, url: asset.sourceUrl, message: String(error) })
  }
})
if (failures.length) {
  throw new Error(`asset import failures:\n${failures.map((f) => `${f.id}: ${f.message}`).join("\n")}`)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of unionlabs/union@031785bb6d (2026-08-16). Data as JSON: /api/errors/86105b0b50b928b4. Report an issue: GitHub.