{"record":{"id":"9be014b2bbd74f51","repo":"paperclipai/paperclip","slug":"attachment-not-ready","errorCode":"attachment_not_ready","errorMessage":"Photon attachment is still being prepared; retry download","messagePattern":"Photon attachment is still being prepared; retry download","errorType":"error_code","errorClass":"PhotonError","httpStatus":null,"severity":"warning","filePath":"server/src/services/photon/attachments.ts","lineNumber":173,"sourceCode":"        if (!header) throw new Error(\"Photon attachment header is missing\");\n        length += part.data.length;\n        if (length > MAX_ATTACHMENT_BYTES)\n          throw new Error(\"Attachment exceeds the configured size limit\");\n        chunks.push(part.data);\n      } else if (part.type === \"companionChunk\") {\n        if (!header || !companionInfo)\n          throw new Error(\"Photon companion metadata is missing\");\n        companionStarted = true;\n        companionLength += part.data.length;\n        if (companionUnavailable || companionLength > MAX_ATTACHMENT_BYTES) {\n          companionUnavailable = true;\n          break;\n        }\n        companionChunks.push(part.data);\n      }\n    }\n    if (timedOut || !header || !length)\n      throw new PhotonError(\n        \"attachment_not_ready\",\n        \"Photon attachment is still being prepared; retry download\",\n      );\n    if (attachment.totalBytes > 0 && length !== attachment.totalBytes)\n      throw new PhotonError(\n        \"attachment_not_ready\",\n        \"Photon attachment transfer is incomplete\",\n      );\n    if (\n      companionInfo &&\n      !companionUnavailable &&\n      companionLength !== companionInfo.totalBytes\n    )\n      throw new PhotonError(\n        \"attachment_not_ready\",\n        \"Photon Live Photo companion is still being prepared\",\n      );\n    const body = Buffer.concat(chunks);","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/attachments.ts#L155-L191","documentation":"After the stream ends, the code checks for timeout, presence of a header, and at least one byte of primary data. If the 30s timer fired, no header arrived, or zero bytes were received, it throws a PhotonError with code attachment_not_ready (server/src/services/photon/attachments.ts:173). This signals the attachment is not yet downloadable (still being prepared server-side) rather than a hard failure.","triggerScenarios":"stream.close() fired by the 30s timeout before data completed; stream ended with no header part; stream ended with length === 0.","commonSituations":"Recipient just sent a large photo and Photon has not finished ingesting it; gateway is slow or cold-starting; network stall triggering the 30s timeout; immediately reacting to a new-message event before the attachment is ready.","solutions":["Retry the download after a short backoff (e.g. 1-5s, a few attempts) — this error is explicitly retryable.","Delay first download attempt for very fresh messages to let Photon finish preparing the attachment.","Increase tolerance for slow gateways (the 30s timeout is fixed in this function; schedule retries around it).","Check gateway health/connectivity if every attempt times out with zero bytes."],"exampleFix":"// before\nconst body = await downloadPhotonAttachment(client, lineId, locator); // throws when not ready\n// after\nlet body;\nfor (let attempt = 0; attempt < 4; attempt++) {\n  try { body = await downloadPhotonAttachment(client, lineId, locator); break; }\n  catch (e) {\n    if (e instanceof PhotonError && e.code === \"attachment_not_ready\") {\n      await new Promise(r => setTimeout(r, 2000 * (attempt + 1)));\n      continue;\n    }\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await downloadPhotonAttachment(client, lineId, locator);\n} catch (e) {\n  if (e instanceof PhotonError && e.code === \"attachment_not_ready\") {\n    await sleep(backoffMs); // exponential, bounded attempts\n    return retryDownload(locator);\n  }\n  throw e;\n}","preventionTips":["Wait briefly after new-message events before first download attempt","Implement exponential backoff with a bounded attempt count for attachment_not_ready","Monitor gateway latency; frequent timeouts indicate infrastructure, not attachment, issues"],"tags":["retryable","timeout","attachments","eventual-consistency"],"backgroundTag":"request-timeout","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}