paperclipai/paperclip · error · HttpError

oauth_access_token_missing

oauth_access_token_missing

Error message

OAuth provider did not return an access token

What it means

The OAuth token exchange response body contained no access_token field, so despite the provider responding, no usable credential was obtained and the exchange is treated as failed.

Solutions

  1. The provider response contained no access token. Check the provider configuration/scopes and retry the authorization.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at server/src/services/tool-access.ts:4454 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-08-18). Data as JSON: /api/errors/388f2807d4c54f59. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/tool-access.ts:4612

          .update(toolMcpGatewayTokens)
          .set({ revokedAt: now, updatedAt: now })
          .where(and(
            eq(toolMcpGatewayTokens.companyId, connection.companyId),
            inArray(toolMcpGatewayTokens.gatewayId, gatewayRows.map((row) => row.id)),
            isNull(toolMcpGatewayTokens.revokedAt),
          ))
          .returning({ id: toolMcpGatewayTokens.id });
        gatewayTokensRevoked = revokedTokens.length;
        if (revokedTokens.length > 0) {
          gatewaySessionsRevoked = (await db
            .update(toolGatewaySessions)
            .set({ revokedAt: now, updatedAt: now })
            .where(and(
              eq(toolGatewaySessions.companyId, connection.companyId),
              inArray(toolGatewaySessions.gatewayTokenId, revokedTokens.map((row) => row.id)),
              isNull(toolGatewaySessions.revokedAt),
            ))
            .returning({ id: toolGatewaySessions.id })).length;
        }
      }
    }

    // A local runtime already holds the injected credential inside a live child
    // process, so archiving rows is not enough — the process itself is an access
    // path. Stopping is best effort on purpose: if the supervisor cannot be
    // reached, revoking the credential anyway (so nothing can start again) beats
    // abandoning the teardown, and the warning says which slot was left running.
    let runtimeSlotsStopped = 0;
    const runtimeSlotRows = await db
      .select({ id: toolRuntimeSlots.id, status: toolRuntimeSlots.status })
      .from(toolRuntimeSlots)
      .where(and(
        eq(toolRuntimeSlots.companyId, connection.companyId),
        eq(toolRuntimeSlots.connectionId, connection.id),
      ));
    for (const slot of runtimeSlotRows) {

View on GitHub (pinned to 01ad858492)