yikart/AiToEarn · error · BilibiliPlatformException
ChannelRefreshTokenFailed
ChannelRefreshTokenFailed
Error message
ResponseCode.ChannelRefreshTokenFailed
What it means
BilibiliService.refreshAccessToken POSTs to /x/account-oauth2/v1/refresh_token to rotate tokens. If the response contains no data it throws BilibiliPlatformException ChannelRefreshTokenFailed (category Auth, cause Platform) — Bilibili refused the refresh.
Source
Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/bilibili/bilibili.service.ts:123
refreshToken: string
expiresAt: Date
}> {
const body = new URLSearchParams({
client_id: this.cfg.clientId,
client_secret: this.cfg.clientSecret,
grant_type: BilibiliOAuthGrantType.RefreshToken,
refresh_token: refreshToken,
})
const response = await this.platformHttp.post<BilibiliApiResponse<{
access_token: string
refresh_token: string
expires_in: number
}>>('https://api.bilibili.com/x/account-oauth2/v1/refresh_token', body.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
if (!response.data.data) {
throw new BilibiliPlatformException({
code: ResponseCode.ChannelRefreshTokenFailed,
category: PlatformErrorCategory.Auth,
context: { endpoint: 'POST https://api.bilibili.com/x/account-oauth2/v1/refresh_token' },
cause: { type: PlatformErrorCauseType.Platform },
})
}
const { access_token, refresh_token, expires_in } = response.data.data
return {
accessToken: access_token,
refreshToken: refresh_token,
expiresAt: new Date(Date.now() + expires_in * 1000),
}
}
async revokeToken(accessToken: string): Promise<void> {
// Bilibili Open Platform does not expose a server-side revoke endpoint for this OAuth token.View on GitHub (pinned to d3aa8bea5b)
Solutions
- Persist the NEW refresh token returned by every refresh call — losing it breaks subsequent refreshes.
- On ChannelRefreshTokenFailed, fall back to a full re-authorization (OAuth) for that account.
- Check the stored refreshToken is current and not already consumed; restore from logs/backups if rotated incorrectly.
- Log Bilibili's full response to capture its error code (e.g. refresh token expired) for diagnosis.
Example fix
// before
const r = await refresh(oldRefreshToken)
// new refreshToken discarded
// after
const r = await refresh(oldRefreshToken)
await credentialRepo.update(userId, { refreshToken: r.refreshToken }) // persist rotated token Defensive patterns
Strategy: fallback
Validate before calling
if (!credential.refreshToken) {
await startReauthorization(credential.userId)
return
} Try / catch
try {
tokens = await bilibiliService.refreshAccessToken(credential.refreshToken)
await credentialRepo.update(userId, { refreshToken: tokens.refreshToken })
} catch (e) {
if (e.code === 'ChannelRefreshTokenFailed') {
await markCredentialNeedsReauth(userId) // fallback: full re-auth
} else throw e
} Prevention
- Always persist the rotated refresh token returned by each refresh call
- Handle refresh failure by scheduling re-authorization, not endless retries
- Alert on refresh failures to catch revoked/expired grants early
When it happens
Trigger: Credential refresh (getUserAccessToken path) sends a refreshToken that Bilibili rejects: token expired/revoked, already used (single-use rotation), or invalid for the app credentials — the endpoint replies without data.
Common situations: Refresh token expired (Bilibili refresh tokens have limited validity); refresh token consumed in a prior refresh but the new one was not persisted; user revoked the app; account password change invalidates tokens.
Related errors
- ChannelAuthRefreshTokenMissing
- ChannelAccessTokenFailed
- ChannelAuthRefreshTokenMissing
- ChannelAuthRefreshTokenMissing
- ChannelAuthRefreshTokenMissing
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/eefb889bc277bd88.
Report an issue: GitHub.