{"record":{"id":"23b99e1dc43cd41e","repo":"paperclipai/paperclip","slug":"invalid-response-truncated-photon-recovery-frame","errorCode":"invalid_response","errorMessage":"Truncated Photon recovery frame","messagePattern":"Truncated Photon recovery frame","errorType":"error_code","errorClass":"PhotonError","httpStatus":null,"severity":"error","filePath":"server/src/services/photon/recovery-transport.ts","lineNumber":28,"sourceCode":"  type PhotonLineAuthentication,\n} from \"./cloud.js\";\nimport { MAX_ATTACHMENT_BYTES } from \"../../attachment-types.js\";\n\nexport type PhotonRecoveryEvent =\n  | CatchUpEvent\n  | { type: \"photon.ignored\"; sequence: number };\nexport const PHOTON_CATCHUP_PATH =\n  \"/photon.imessage.v1.EventService/CatchUpEvents\";\n\n/** Pinned v2.1.0 protobuf envelope. The SDK decoder owns the event graph; this\n * reader retains sequence-only/new-variant frames that its public API drops. */\nexport function photonEnvelopeSequence(bytes: Uint8Array): number | undefined {\n  let offset = 0;\n  const integer = () => {\n    let value = 0n;\n    for (let shift = 0n; shift < 70n; shift += 7n) {\n      if (offset >= bytes.length)\n        throw new PhotonError(\n          \"invalid_response\",\n          \"Truncated Photon recovery frame\",\n        );\n      const byte = bytes[offset++];\n      value |= BigInt(byte & 127) << shift;\n      if (!(byte & 128)) {\n        if (value > BigInt(Number.MAX_SAFE_INTEGER))\n          throw new PhotonError(\n            \"history_gap\",\n            \"Photon recovery sequence exceeds the supported range\",\n          );\n        return Number(value);\n      }\n    }\n    throw new PhotonError(\n      \"invalid_response\",\n      \"Invalid Photon recovery integer\",\n    );","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/recovery-transport.ts#L10-L46","documentation":"Photon recovery frames encode integers as LEB128/varint bytes. While decoding a varint, if the buffer ends before a continuation byte (high bit clear) is found, the frame is truncated and the decoder throws invalid_response, since a partial integer cannot be trusted for sequence parsing.","triggerScenarios":"photonEnvelopeSequence() (or length()/tag() via the integer() helper) is called with a Uint8Array that cuts off mid-varint: the loop reaches offset >= bytes.length while the current byte still has the continuation bit (byte & 128) set.","commonSituations":"A transport layer truncated the recovery frame (partial read from a socket, HTTP body cut off); a caller passing a wrong-length slice of a larger buffer; a buggy frame serializer on the Photon side; mixing frame-length metadata with actual payload length after a protocol change.","solutions":["Check the transport: ensure the full frame is read according to its length prefix before calling photonEnvelopeSequence()","Log bytes.length and the frame's declared length; if they differ, fix the framing/slicing code on the client","Verify the Photon server version — a serializer bug can emit truncated frames; upgrade if a fix exists","Add a frame-level checksum/length assertion before decoding to fail fast at the transport boundary"],"exampleFix":"// before: decoding a partial slice\nconst seq = photonEnvelopeSequence(buffer.subarray(0, headerLen));\n// after: decode only a fully received frame\nif (buffer.length < expectedFrameLength) throw new Error(\"frame incomplete; wait for more bytes\");\nconst seq = photonEnvelopeSequence(buffer.subarray(0, expectedFrameLength));","handlingStrategy":"validation","validationCode":"function frameComplete(bytes: Uint8Array, declaredLength: number): boolean {\n  return bytes.length >= declaredLength;\n}\n// only call photonEnvelopeSequence once the full frame has been received","typeGuard":"function isCompleteFrame(buf: Uint8Array): boolean {\n  if (buf.length < 1) return false;\n  let offset = 0;\n  while (offset < buf.length) {\n    if (!(buf[offset] & 128)) return true; // last varint byte terminated\n    offset++;\n  }\n  return false;\n}","tryCatchPattern":"try {\n  const seq = photonEnvelopeSequence(frame);\n} catch (e) {\n  if (e instanceof PhotonError && e.code === \"invalid_response\" && /Truncated/.test(e.message)) {\n    await discardFrameAndRequestReplay(); // transport delivered a partial frame\n  } else throw e;\n}","preventionTips":["Always read frames to their declared length before decoding","Assert received byte count equals the frame length prefix","Add a checksum over each frame to detect transport truncation","Keep client and server frame-format versions in lockstep"],"tags":["photon","varint","truncated-frame","invalid-response"],"backgroundTag":"protobuf-unmarshal-failed","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}