tinyhumansai/openhuman · error · Error
asset upload failed: ${res.status} ${res.statusText} ${await
Error message
asset upload failed: ${res.status} ${res.statusText} ${await res.text()} What it means
uploadOperation() executes one reserved upload operation: it slices fileBuffer at [offset, offset+length] and PUTs the chunk to the signed Apple object-store URL with the exact requestHeaders ASC returned. A non-ok PUT response throws with status, statusText and the raw body — unlike error 268 this comes from the storage backend, not the ASC JSON API.
Source
Thrown at scripts/ios-appstore-metadata.mjs:100
throw new Error(`${method} ${resourcePath} failed: ${message}`);
}
return payload;
}
async function uploadOperation(operation, fileBuffer) {
const headers = Object.fromEntries(
(operation.requestHeaders || []).map((h) => [h.name, h.value]),
);
const offset = Number(operation.offset || 0);
const length = Number(operation.length || fileBuffer.length);
const chunk = fileBuffer.subarray(offset, offset + length);
const res = await fetch(operation.url, {
method: operation.method,
headers,
body: chunk,
});
if (!res.ok) {
throw new Error(
`asset upload failed: ${res.status} ${res.statusText} ${await res.text()}`,
);
}
}
async function textFile(name) {
return (await readFile(path.join(metadataDir, name), "utf8")).trim();
}
async function firstPage(resourcePath) {
const payload = await request("GET", resourcePath);
return payload.data || [];
}
async function getOrCreateAppInfoLocalization() {
const appInfos = await firstPage(`/apps/${appId}/appInfos?limit=10`);
if (!appInfos.length) {
throw new Error(`No appInfos found for app ${appId}.`);View on GitHub (pinned to a221052e0d)
Solutions
- Re-run the whole script end-to-end so fresh upload operations are reserved and used within the same run — do not resume a half-finished run with changed files
- Keep fastlane/screenshots/en-US untouched while the script runs (it deletes and recreates the screenshot set each run, so a clean re-run is safe)
- On persistent 403 with 'Request has expired' in the body, check system clock sync, then re-run
- If the buffer/length mismatch repeats, verify the PNGs are fully written (not mid-copy) before starting
Example fix
# before — partial run, then files changed: node scripts/ios-appstore-metadata.mjs # fails midway # (regenerate screenshots here) node scripts/ios-appstore-metadata.mjs # upload fails: stale ops # after — single clean run over stable files: # (finish all screenshot edits first) node scripts/ios-appstore-metadata.mjs
Defensive patterns
Strategy: retry
Validate before calling
// Guard before each PUT: operation must fit the current buffer
const ok = operation.offset + operation.length <= fileBuffer.length;
if (!ok) { console.error('Stale upload operations vs. file size — restart the run'); process.exit(1); } Try / catch
async function uploadWithRetry(operation, buf, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try { return await uploadOperation(operation, buf); }
catch (err) {
const transient = /\b(5\d\d|429)\b/.test(err.message);
if (!transient || i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, 2 ** i * 1000));
}
}
} Prevention
- Run the script end-to-end in one go — never resume a partial upload run after modifying the local PNGs, because reserved operations and file bytes must match
- Freeze fastlane/screenshots/en-US before starting; any change mid-run causes length/offset mismatches
- Retry only on 5xx/429 with backoff; a persistent 403 with 'expired' in the body means re-reserve the screenshot set (re-run)
When it happens
Trigger: Signed upload operations expiring because a long time passed between createScreenshotSet() and the PUT; the local PNG file being regenerated/changed between reserve and upload so Content-Length no longer matches; offset/length from the reserve response exceeding the file's size; transient 5xx from the CDN.
Common situations: Re-running after a partial failure but with new screenshots on disk; large screenshot sets over slow links where later operations go stale; system clock skew invalidating signed URLs.
Related errors
- ${method} ${resourcePath} failed: ${message}
- No screenshots found in ${screenshotDir}
- ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_PATH are required.
- No appInfos found for app ${appId}.
- Failed to send magic link (${response.status})
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/0912928f5c1f49f7.
Report an issue: GitHub.