{"record":{"id":"0912928f5c1f49f7","repo":"tinyhumansai/openhuman","slug":"asset-upload-failed-res-status-res-statustex","errorCode":null,"errorMessage":"asset upload failed: ${res.status} ${res.statusText} ${await res.text()}","messagePattern":"asset upload failed: (.+?) (.+?) (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/ios-appstore-metadata.mjs","lineNumber":100,"sourceCode":"    throw new Error(`${method} ${resourcePath} failed: ${message}`);\n  }\n  return payload;\n}\n\nasync function uploadOperation(operation, fileBuffer) {\n  const headers = Object.fromEntries(\n    (operation.requestHeaders || []).map((h) => [h.name, h.value]),\n  );\n  const offset = Number(operation.offset || 0);\n  const length = Number(operation.length || fileBuffer.length);\n  const chunk = fileBuffer.subarray(offset, offset + length);\n  const res = await fetch(operation.url, {\n    method: operation.method,\n    headers,\n    body: chunk,\n  });\n  if (!res.ok) {\n    throw new Error(\n      `asset upload failed: ${res.status} ${res.statusText} ${await res.text()}`,\n    );\n  }\n}\n\nasync function textFile(name) {\n  return (await readFile(path.join(metadataDir, name), \"utf8\")).trim();\n}\n\nasync function firstPage(resourcePath) {\n  const payload = await request(\"GET\", resourcePath);\n  return payload.data || [];\n}\n\nasync function getOrCreateAppInfoLocalization() {\n  const appInfos = await firstPage(`/apps/${appId}/appInfos?limit=10`);\n  if (!appInfos.length) {\n    throw new Error(`No appInfos found for app ${appId}.`);","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/scripts/ios-appstore-metadata.mjs#L82-L118","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before — partial run, then files changed:\nnode scripts/ios-appstore-metadata.mjs   # fails midway\n# (regenerate screenshots here)\nnode scripts/ios-appstore-metadata.mjs   # upload fails: stale ops\n\n# after — single clean run over stable files:\n# (finish all screenshot edits first)\nnode scripts/ios-appstore-metadata.mjs","handlingStrategy":"retry","validationCode":"// Guard before each PUT: operation must fit the current buffer\nconst ok = operation.offset + operation.length <= fileBuffer.length;\nif (!ok) { console.error('Stale upload operations vs. file size — restart the run'); process.exit(1); }","typeGuard":null,"tryCatchPattern":"async function uploadWithRetry(operation, buf, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    try { return await uploadOperation(operation, buf); }\n    catch (err) {\n      const transient = /\\b(5\\d\\d|429)\\b/.test(err.message);\n      if (!transient || i === attempts - 1) throw err;\n      await new Promise(r => setTimeout(r, 2 ** i * 1000));\n    }\n  }\n}","preventionTips":["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)"],"tags":["app-store-connect","upload","network","release","screenshots"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}