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

  1. Restart the connection sign-in flow to mint a fresh handoff session.
  2. Clear the stale pending handoff from sessionStorage before retrying.
  3. Avoid double-invoking the start/handoff step; debounce the start button.
  4. 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

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


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