paperclipai/paperclip · error

Photon attachment metadata changed

Error message

Photon attachment metadata changed

What it means

When the download stream emits its header part, its metadata must match expectations exactly: it must be the first header, part.info.guid must equal locator.attachmentGuid (or match a validated shared-gateway alias), the part must not be hidden/sticker, and totalBytes must be a safe in-range integer. Any mismatch throws this error (server/src/services/photon/attachments.ts:139), because the streamed bytes would not correspond to the attachment requested.

Solutions

  1. Re-fetch the message metadata and rebuild the locator so the attachmentGuid matches what the stream header reports.
  2. If using allocation "shared", confirm the spc-att-* alias and that fileName/mimeType/totalBytes still match the message attachment metadata; otherwise use allocation "dedicated".
  3. Upgrade/align the Photon gateway version so header GUIDs are native UUIDs consistent with message metadata.
  4. Do not reuse a partially consumed download stream; open a fresh client.attachments.downloadStream per attempt.
Defensive patterns

Strategy: retry

Validate before calling

const att = (message.raw as PhotonMessage).content.attachments.find(a => a.guid === locator.attachmentGuid);
if (!att) throw new Error("no such attachment; header match will fail");

Try / catch

try {
  await downloadPhotonAttachment(client, lineId, locator);
} catch (e) {
  if (e instanceof Error && e.message === "Photon attachment metadata changed") {
    // re-fetch message metadata, rebuild locator, retry once; else mark unavailable
  } else throw e;
}

Prevention

When it happens

Trigger: A second header part arrives (header already true); stream header guid differs from locator.attachmentGuid without a valid shared alias match (spc-att-* alias plus matching native UUID, totalBytes, mimeType, fileName); header part is hidden/sticker; header totalBytes is invalid or over the cap.

Common situations: Using a shared-gateway alias attachmentGuid where the gateway rewrote IDs and the alias-validation fields (mimeType/fileName/totalBytes) changed after the message lookup; gateway version mismatch emitting rewritten GUIDs in headers; retrying a stream that already delivered a header on a stale stream object.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/0e9331ef4fce3fa0. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/photon/attachments.ts:139

        // The shared gateway rewrites message/metadata attachment IDs to opaque
        // project aliases, but streams the native UUID in download headers.
        // Ownership comes from the authenticated source-message lookup above
        // and this exact alias-addressed RPC, never from matching filenames.
        const sharedAlias = allocation === "shared" &&
          /^spc-att-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(locator.attachmentGuid);
        const matchingSharedHeader = sharedAlias &&
          /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(part.info.guid) &&
          part.info.totalBytes === attachment.totalBytes &&
          part.info.mimeType === attachment.mimeType &&
          part.info.fileName === attachment.fileName;
        if (
          header ||
          (part.info.guid !== locator.attachmentGuid && !matchingSharedHeader) ||
          part.info.isHidden || part.info.isSticker ||
          !Number.isSafeInteger(part.info.totalBytes) || part.info.totalBytes < 0 ||
          part.info.totalBytes > MAX_ATTACHMENT_BYTES
        )
          throw new Error("Photon attachment metadata changed");
        header = true;
        companionInfo = part.companionInfo;
        companionUnavailable = Boolean(
          companionInfo &&
            (companionInfo.kind !== "live-photo-video" ||
              !["video/quicktime", "video/mp4"].includes(
                companionInfo.mimeType,
              ) ||
              !Number.isSafeInteger(companionInfo.totalBytes) ||
              companionInfo.totalBytes <= 0 ||
              companionInfo.totalBytes > MAX_ATTACHMENT_BYTES),
        );
      } else if (part.type === "primaryChunk") {
        if (companionStarted)
          throw new Error("Photon attachment chunks arrived out of order");
        if (!header) throw new Error("Photon attachment header is missing");
        length += part.data.length;
        if (length > MAX_ATTACHMENT_BYTES)

View on GitHub (pinned to 3f1d897a7c)