paperclipai/paperclip · error · OAuthHandoffError

invalid_handoff

invalid_handoff

Error message

Paperclip Cloud returned an invalid sign-in handoff.

What it means

parseHandoff validates the shape of a Paperclip Cloud sign-in handoff object. If the value is present but not a plain object (null, non-object, or array), it throws OAuthHandoffError with code "invalid_handoff" because the handoff payload from the server (or storage) is structurally corrupt and cannot be used to resume sign-in.

Source

Thrown at ui/src/lib/oauthHandoff.ts:37

export class OAuthHandoffError extends Error {
  constructor(
    message: string,
    readonly code:
      | "invalid_handoff"
      | "expired"
      | "forbidden"
      | "unavailable",
  ) {
    super(message);
    this.name = "OAuthHandoffError";
  }
}

function parseHandoff(value: unknown): { kind: "paperclip_cloud"; session: string } | null {
  if (value === undefined) return null;
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new OAuthHandoffError("Paperclip Cloud returned an invalid sign-in handoff.", "invalid_handoff");
  }
  const handoff = value as Record<string, unknown>;
  if (
    handoff.kind !== "paperclip_cloud"
    || typeof handoff.session !== "string"
    || handoff.session.length < 16
    || handoff.session.length > 512
    || !/^[A-Za-z0-9_-]+$/.test(handoff.session)
  ) {
    throw new OAuthHandoffError("Paperclip Cloud returned an invalid sign-in handoff.", "invalid_handoff");
  }
  return { kind: "paperclip_cloud", session: handoff.session };
}

function handoffFailure(status: number, code: unknown): OAuthHandoffError {
  if (status === 404 || code === "SESSION_NOT_AVAILABLE") {
    return new OAuthHandoffError("This sign-in expired. Start the connection again.", "expired");
  }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Log/inspect the raw handoff value returned by the start endpoint and fix the producer to return `{ kind: "paperclip_cloud", session }`.
  2. Clear stale sessionStorage entries (the pending handoff key) and restart sign-in.
  3. Wrap parseHandoff callers in try/catch for OAuthHandoffError and fall back to a fresh sign-in.

Example fix

// before
const handoff = JSON.parse(raw); // could be anything
await prepareOAuthNavigation({ handoff, authorizationUrl });
// after
let parsed: unknown;
try { parsed = JSON.parse(raw); } catch { parsed = null; }
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
  await prepareOAuthNavigation({ handoff: parsed, authorizationUrl });
} else {
  sessionStorage.removeItem(PENDING_HANDOFF_KEY); // start fresh
}
Defensive patterns

Strategy: type-guard

Validate before calling

function looksLikeHandoff(v: unknown): boolean {
  return !!v && typeof v === "object" && !Array.isArray(v);
}

Type guard

function isCloudHandoff(v: unknown): v is { kind: "paperclip_cloud"; session: string } {
  return !!v && typeof v === "object" && !Array.isArray(v)
    && (v as any).kind === "paperclip_cloud"
    && typeof (v as any).session === "string";
}

Try / catch

try {
  const handoff = readPendingCloudHandoff(sessionStorage);
} catch (e) {
  if (e instanceof OAuthHandoffError && e.code === "invalid_handoff") {
    sessionStorage.removeItem(PENDING_HANDOFF_KEY); // discard corrupt payload
  } else throw e;
}

Prevention

When it happens

Trigger: Calling parseHandoff (via prepareOAuthNavigation or readPendingCloudHandoff) with `start.handoff` or a stored pending-handoff value that is null, an array, or a primitive instead of an object.

Common situations: Backend bug returning handoff as a string; sessionStorage corruption or schema change leaving non-object JSON; API version mismatch where handoff shape changed.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/93170802e76acbf7. Report an issue: GitHub.