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
- Reconnect the connection through Paperclip Cloud (managed connector enrollment flow) to obtain a fresh refresh token for the current client
- Re-enroll the connector as a managed connector if it is still flagged as legacy
- Until reconnected, rely on the unexpired access token for short-lived operations
- 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
- Migrate legacy OAuth grants to managed connectors proactively after enabling Paperclip Cloud
- Monitor grants with status needs_reauthorization and alert
- Track legacy client identifiers in grants and flag them before access tokens expire
- Re-connect connections after any Google client/app reconfiguration
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
- REAUTHORIZATION_REQUIRED
- connector_profile_invalid
- agent_authorization_required
- / : oauth requires ownershipModes
- CONNECTOR_BAD_RESPONSE
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)