paperclipai/paperclip · warning · OAuthHandoffError
expired
expired
Error message
This sign-in expired. Start the connection again.
What it means
prepareOAuthNavigation resolves the sign-in destination. When the start result includes a cloud handoff, it POSTs the session to Paperclip Cloud; if the cloud responds with an error indicating the session is no longer valid (404 or SESSION_NOT_AVAILABLE via handoffFailure), the user-facing error is code "expired" — the sign-in must be restarted. Handoff sessions are short-lived by design.
Source
Thrown at ui/src/lib/oauthHandoff.ts:125
return null;
}
}
/**
* Resolve a start response into the next browser navigation.
*
* Managed Cloud sessions are exchanged only through the fixed same-origin
* endpoint. Legacy, self-hosted, and direct provider OAuth keep using the
* server-supplied authorization URL after the existing URL safety gate.
*/
export async function prepareOAuthNavigation(
start: Pick<ToolOAuthStartResult, "authorizationUrl" | "handoff">,
options: { signal?: AbortSignal; request?: typeof fetch } = {},
): Promise<PreparedOAuthNavigation> {
const handoff = parseHandoff(start.handoff);
if (!handoff) {
const target = resolveAuthorizationTarget(start.authorizationUrl);
if (!target.ok) throw new OAuthHandoffError(target.message, "invalid_handoff");
return { kind: "authorization", url: target.url, host: target.host };
}
const response = await postCloudHandoff(handoff.session, options);
const body = await response.json().catch(() => null) as Record<string, unknown> | null;
if (!response.ok) {
if (body?.error === "RECENT_LOGIN_REQUIRED") {
const reauthentication = exactReauthenticationTarget(body.reauthenticationUrl, handoff.session);
if (reauthentication) return reauthentication;
}
throw handoffFailure(response.status, body?.error);
}
const authorization = resolveAuthorizationTarget(
typeof body?.authorizationUrl === "string" ? body.authorizationUrl : undefined,
);
if (!authorization.ok) {
throw new OAuthHandoffError("Paperclip Cloud returned an invalid provider sign-in address.", "invalid_handoff");
}View on GitHub (pinned to 01ad858492)
Solutions
- Restart the connection sign-in flow to mint a fresh handoff session.
- Clear the stale pending handoff from sessionStorage before retrying.
- Avoid double-invoking the start/handoff step; debounce the start button.
- Catch OAuthHandoffError code "expired" in UI and auto-restart the flow.
Example fix
// before
await prepareOAuthNavigation(start); // throws if session expired
// after
try {
await prepareOAuthNavigation(start);
} catch (e) {
if (e instanceof OAuthHandoffError && e.code === "expired") {
clearPendingCloudHandoff();
await restartConnectionSignIn();
} else throw e;
} Defensive patterns
Strategy: retry
Validate before calling
const pending = readPendingCloudHandoffSafe(sessionStorage);
if (pending && Date.now() - pending.savedAt > MAX_HANDOFF_TTL_MS) {
clearPendingCloudHandoff(); // stale — restart sign-in proactively
} Type guard
function isFreshHandoff(p: { savedAt: number } | null, ttlMs = 5 * 60_000): p is { savedAt: number } {
return !!p && Date.now() - p.savedAt < ttlMs;
} Try / catch
try {
await prepareOAuthNavigation(start);
} catch (e) {
if (e instanceof OAuthHandoffError && e.code === "expired") {
clearPendingCloudHandoff();
await restartSignIn(); // mint a fresh session transparently
} else throw e;
} Prevention
- Expire pending sessionStorage handoffs client-side using savedAt + a conservative TTL.
- Debounce the start control so a double-click cannot consume the session twice.
- Auto-restart the flow on code "expired" instead of dead-ending the user.
- Avoid long idle periods between starting sign-in and completing it.
When it happens
Trigger: Calling prepareOAuthNavigation with a handoff whose session was already consumed, superseded by a newer handoff, or aged past the cloud's session TTL, causing the POST to return 404 / SESSION_NOT_AVAILABLE.
Common situations: User left the setup dialog open too long before clicking through; double-clicking start so the first POST consumed the session; resuming a pending handoff saved in sessionStorage from an earlier (expired) attempt; server restarted with in-memory session store.
Related errors
- Sign in again to connect your own account.
- live_provider_execution_failed
- device-login credential promotion rejected: ${result.outcome
- OAuth callback connection belongs to a missing company
- Discord bot is not installed in the selected server
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/98a9fcb607642800.
Report an issue: GitHub.