paperclipai/paperclip · error · ToolGatewayHttpError

connector_reauthorization_required

connector_reauthorization_required

Error message

Legacy authorization must be reconnected through Paperclip Cloud

What it means

Thrown by the tool gateway when a Gmail/Google connector grant was created under the legacy (unmanaged) OAuth client. Its refresh token cannot be exchanged through Paperclip Cloud, so the gateway marks the grant needs_reauthorization and refuses to refresh it, telling the caller to re-enroll the connector as a managed connector and reconnect.

Solutions

  1. Reconnect the connection through Paperclip Cloud (managed connector enrollment flow) to obtain a fresh refresh token for the current client
  2. Re-enroll the connector as a managed connector if it is still flagged as legacy
  3. Until reconnected, rely on the unexpired access token for short-lived operations
  4. Re-create the connection from scratch if reconnect is unavailable

Example fix

// before
const grant = await getGrant(connectionId); // legacy grant, refresh throws 409
await gateway.refresh(connectionId);
// after
if (grant.isLegacy) {
  await reconnectViaPaperclipCloud(connectionId); // managed-connector enrollment
}
await gateway.refresh(connectionId);
Defensive patterns

Strategy: try-catch

Validate before calling

const grant = await getGrant(connId);
if (grant.status === "needs_reauthorization" || grant.isLegacy) await reconnectViaPaperclipCloud(connId);

Type guard

function isReauthRequired(e: unknown): e is ToolGatewayHttpError {
  return e instanceof ToolGatewayHttpError && e.code === "connector_reauthorization_required";
}

Try / catch

try { await gateway.callTool(session, connId, p); }
catch (e) {
  if (e instanceof ToolGatewayHttpError && e.code === "connector_reauthorization_required") {
    await reconnectManagedConnection(connId);
  } else throw e;
}

Prevention

When it happens

Trigger: Any managed token refresh attempt (refreshManagedGmailGrant path) on a grant whose credential refs carry the legacy client identity: the refresh token is from a different Google client than the current Paperclip Cloud connector, so exchanging it is rejected.

Common situations: An instance migrated from self-hosted/legacy Google OAuth setup to Paperclip Cloud managed connectors; a Google app re-configuration changed the client ID behind existing stored grants; grants created before managed-connector enrollment are still in use after their access token expired.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/489a9d360a934ce1. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/tool-gateway.ts:3638

      Number.isFinite(expiresAt) &&
      expiresAt > currentTime + 60 * 60_000 &&
      !rotationDue
    )
      return grant;
    if (oauth.strategy === "paperclip_id_connector") {
      // Paperclip ID used different endpoints, signing metadata, envelope
      // purposes, and a different Google client. Its refresh token cannot be
      // exchanged through Paperclip Cloud. Let an unexpired access token finish
      // its useful life, then require an explicit managed-connector enrollment
      // and provider reconnect instead of sending it to the wrong client.
      await db
        .update(connectionGrants)
        .set({
          status: "needs_reauthorization",
          updatedAt: new Date(currentTime),
        })
        .where(eq(connectionGrants.id, grant.id));
      throw new ToolGatewayHttpError(
        409,
        "Legacy authorization must be reconnected through Paperclip Cloud",
        "connector_reauthorization_required",
        {
          connectionId: connection.id,
          grantId: grant.id,
        },
      );
    }
    const existingFlight = gmailRefreshFlights.get(grant.id);
    if (existingFlight) return existingFlight;
    const refresh = (async () => {
      const cloudConnector = currentCloudConnector();
      if (!cloudConnector || !connectorSubject) {
        await db
          .update(connectionGrants)
          .set({
            status: "needs_reauthorization",

View on GitHub (pinned to 3f1d897a7c)