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
- Log/inspect the raw handoff value returned by the start endpoint and fix the producer to return `{ kind: "paperclip_cloud", session }`.
- Clear stale sessionStorage entries (the pending handoff key) and restart sign-in.
- 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
- Validate the handoff shape at the API boundary before storing or forwarding it.
- Version the sessionStorage payload and drop entries with unknown shapes.
- Add contract tests asserting the start endpoint returns `{ kind, session }`.
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
- runnerd-native runtime requests require a canonical question
- connector_profile_invalid
- Invalid status '${String(rawStatus)}'. Must be one of: ${PLU
- "tool" is required and must be a string
- "runContext" is required and must be an object
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/93170802e76acbf7.
Report an issue: GitHub.