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

  1. Check the transport: ensure the full frame is read according to its length prefix before calling photonEnvelopeSequence()
  2. Log bytes.length and the frame's declared length; if they differ, fix the framing/slicing code on the client
  3. Verify the Photon server version — a serializer bug can emit truncated frames; upgrade if a fix exists
  4. 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

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)