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
- 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
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
- 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
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
- The Contentful export is missing the landing or learn entry
- Blog entry ${item.sys.id} has no slug
- Usage: node site/scripts/import-contentful-export.mjs <previ
- Failed to download asset ${asset.id}: ${response.status} ${r
AI-assisted analysis of unionlabs/union@031785bb6d (2026-08-16).
Data as JSON: /api/errors/86105b0b50b928b4.
Report an issue: GitHub.