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?: stringView on GitHub (pinned to d3aa8bea5b)
Solutions
- 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.
- Ensure the redirectUri passed to createOAuth2Client is byte-identical to the one registered in the Twitter developer portal and the one used in generateAuthUrl.
- Treat each authorization code as single-use: don't retry exchangeCode after a failure; restart the whole authorize flow.
- Persist codeVerifier keyed by the state parameter and verify the callback state matches before exchanging.
- 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
- Bind codeVerifier to the OAuth state parameter server-side and validate state on callback.
- Never retry exchangeCode with a used/failed code; restart the authorize flow instead.
- Redirect fast after callback — the code expires in ~30 seconds.
- Compare redirectUri strings exactly (including trailing slash) across authorize and token requests.
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
- 15009
- ChannelAccessTokenFailed
- ChannelAccessTokenFailed
- ChannelPlatformApiFailed
- ChannelAuthRefreshTokenMissing
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/1fc5aa4306bbaf2f.
Report an issue: GitHub.