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

  1. Persist the NEW refresh token returned by every refresh call — losing it breaks subsequent refreshes.
  2. On ChannelRefreshTokenFailed, fall back to a full re-authorization (OAuth) for that account.
  3. Check the stored refreshToken is current and not already consumed; restore from logs/backups if rotated incorrectly.
  4. 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

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


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