paperclipai/paperclip · error · PhotonError
invalid_response
invalid_response
Error message
Truncated Photon recovery frame
What it means
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.
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
Example fix
// before: decoding a partial slice
const seq = photonEnvelopeSequence(buffer.subarray(0, headerLen));
// after: decode only a fully received frame
if (buffer.length < expectedFrameLength) throw new Error("frame incomplete; wait for more bytes");
const seq = photonEnvelopeSequence(buffer.subarray(0, expectedFrameLength)); Defensive patterns
Strategy: validation
Validate before calling
function frameComplete(bytes: Uint8Array, declaredLength: number): boolean {
return bytes.length >= declaredLength;
}
// only call photonEnvelopeSequence once the full frame has been received Type guard
function isCompleteFrame(buf: Uint8Array): boolean {
if (buf.length < 1) return false;
let offset = 0;
while (offset < buf.length) {
if (!(buf[offset] & 128)) return true; // last varint byte terminated
offset++;
}
return false;
} Try / catch
try {
const seq = photonEnvelopeSequence(frame);
} catch (e) {
if (e instanceof PhotonError && e.code === "invalid_response" && /Truncated/.test(e.message)) {
await discardFrameAndRequestReplay(); // transport delivered a partial frame
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "cannot parse invalid wire-format data", "cannot unmarshal", "failed unmarshalling": protobuf unmarshal errors explained — this error's family across 10 libraries.
Related errors
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/23b99e1dc43cd41e.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/photon/recovery-transport.ts:28
type PhotonLineAuthentication,
} from "./cloud.js";
import { MAX_ATTACHMENT_BYTES } from "../../attachment-types.js";
export type PhotonRecoveryEvent =
| CatchUpEvent
| { type: "photon.ignored"; sequence: number };
export const PHOTON_CATCHUP_PATH =
"/photon.imessage.v1.EventService/CatchUpEvents";
/** Pinned v2.1.0 protobuf envelope. The SDK decoder owns the event graph; this
* reader retains sequence-only/new-variant frames that its public API drops. */
export function photonEnvelopeSequence(bytes: Uint8Array): number | undefined {
let offset = 0;
const integer = () => {
let value = 0n;
for (let shift = 0n; shift < 70n; shift += 7n) {
if (offset >= bytes.length)
throw new PhotonError(
"invalid_response",
"Truncated Photon recovery frame",
);
const byte = bytes[offset++];
value |= BigInt(byte & 127) << shift;
if (!(byte & 128)) {
if (value > BigInt(Number.MAX_SAFE_INTEGER))
throw new PhotonError(
"history_gap",
"Photon recovery sequence exceeds the supported range",
);
return Number(value);
}
}
throw new PhotonError(
"invalid_response",
"Invalid Photon recovery integer",
);View on GitHub (pinned to 3f1d897a7c)