yikart/AiToEarn · error · TwitterPlatformException

15010

15010

Error message

Access token exchange failed

What it means

Thrown by TwitterService.exchangeCode when the OAuth2 authorization code cannot be exchanged for tokens at POST /2/oauth2/token. The SDK's oauth.exchangeCode(code, codeVerifier) throws (Twitter rejects the request), and the service wraps it via fromSdkOAuthError with ResponseCode.ChannelAccessTokenFailed (15010), categorized as Auth and non-retryable. The actual Twitter error_description is preserved in cause.platformMessage/raw.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/twitter/twitter.service.ts:104

  }

  async exchangeCode(
    code: string,
    codeVerifier: string,
  ): Promise<{
    accessToken: string
    refreshToken?: string
    expiresAt?: Date
    scope?: string
  }> {
    let credential: Awaited<ReturnType<OAuth2['exchangeCode']>>
    try {
      const oauth = this.createOAuth2Client()
      credential = await oauth.exchangeCode(code, codeVerifier)
    }
    catch (error) {
      if (error instanceof Error) {
        throw TwitterPlatformException.fromSdkOAuthError(error, {
          code: ResponseCode.ChannelAccessTokenFailed,
          context: { endpoint: 'POST /2/oauth2/token' },
        })
      }
      throw error
    }

    return {
      accessToken: credential.access_token,
      refreshToken: credential.refresh_token,
      expiresAt: credential.expires_in ? new Date(Date.now() + credential.expires_in * 1000) : undefined,
      scope: credential.scope,
    }
  }

  async refreshAccessToken(refreshToken: string): Promise<{
    accessToken: string
    refreshToken?: string

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Read cause.platformMessage for Twitter's error_description (e.g. 'invalid_grant', 'Value passed for the redirect uri did not match') and fix that specific mismatch.
  2. Ensure the redirectUri passed to createOAuth2Client is byte-identical to the one registered in the Twitter developer portal and the one used in generateAuthUrl.
  3. Treat each authorization code as single-use: don't retry exchangeCode after a failure; restart the whole authorize flow.
  4. Persist codeVerifier keyed by the state parameter and verify the callback state matches before exchanging.
  5. Confirm clientId/clientSecret belong to the correct app/environment (CN vs intl) and match where the code was issued.

Example fix

// before: reusing a stale code after a failed/redirected callback
catch (e) { return this.exchangeCode(code, storedVerifier) }
// after: fail fast and require a fresh authorize flow
catch (e) {
  logger.warn(`Token exchange failed: ${e.message}`)
  throw new UnauthorizedException('Please restart the Twitter authorization')
}
Defensive patterns

Strategy: try-catch

Validate before calling

function isExchangeable(code: unknown, codeVerifier: unknown): boolean {
  return typeof code === 'string' && code.length > 0
    && typeof codeVerifier === 'string' && /^[A-Za-z0-9\-._~]{43,128}$/.test(codeVerifier)
}

Type guard

function isAuthFailure(e: unknown): e is ChannelPlatformException & { code: typeof ResponseCode.ChannelAccessTokenFailed } {
  return e instanceof ChannelPlatformException && e.code === ResponseCode.ChannelAccessTokenFailed
}

Try / catch

try {
  const cred = await twitterService.exchangeCode(code, verifierForState(state))
  await saveTokens(userId, cred)
} catch (e) {
  if (isAuthFailure(e)) {
    // codes are single-use: never retry; require a fresh authorize flow
    await clearPendingAuthState(userId)
    throw new UnauthorizedException('Twitter authorization expired, please reconnect')
  }
  throw e
}

Prevention

When it happens

Trigger: Calling exchangeCode with an authorization code that was already redeemed, expired (Twitter codes are valid ~30 seconds), revoked by the user, issued for a different redirect_uri/clientId than configured, or paired with a codeVerifier that does not match the original PKCE challenge.

Common situations: Users refreshing the callback page causing double code redemption; redirect URI in env differing even slightly (trailing slash) from the developer-portal setting; mixing China/intl app credentials or environments; user taking too long between authorize and callback; storing the wrong codeVerifier per state session.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/1fc5aa4306bbaf2f. Report an issue: GitHub.