yikart/AiToEarn · error · Error

无效的状态码

Error message

无效的状态码

What it means

handleAuthorizationCode first validates the OAuth state by reading the Redis key youtube:state:<userId>:<state> populated in getAuthorizationUrl. If the key is missing or has no mail field, it throws Error('无效的状态码') before exchanging the authorization code.

Solutions

  1. Increase the state TTL in getAuthorizationUrl (currently 60*10 seconds) or regenerate state on retry.
  2. Ensure the callback passes the SAME userId used when generating the authorization URL.
  3. Verify Redis persistence (appendonly/AOF) or accept restarts will invalidate pending OAuth flows.
  4. Have users restart the authorization flow when this error occurs; the code exchange cannot proceed without the state.
  5. Return a clear 'state expired, please re-authorize' response to the frontend instead of a generic error.

Example fix

// before
this.redisService.setKey(`youtube:state:${userId}:${state}`, { mail }, 60 * 10);
// after
this.redisService.setKey(`youtube:state:${userId}:${state}`, { mail }, 60 * 30); // 30 min TTL
Defensive patterns

Strategy: validation

Validate before calling

const stateInfo = await redisService.get(`youtube:state:${userId}:${state}`)
if (!stateInfo?.mail) {
  // fail fast with an actionable message before hitting Google
  return { code: 410, msg: '授权状态已过期,请重新发起授权' }
}

Type guard

const hasValidState = (s: unknown): s is { mail: string } =>
  typeof s === 'object' && s !== null && typeof (s as any).mail === 'string' && (s as any).mail.length > 0

Try / catch

try {
  return await youtubeAuthService.handleAuthorizationCode(code, state, userId)
} catch (e) {
  if (e.message === '无效的状态码') {
    // state expired or userId mismatch; redirect user to restart OAuth, never retry the code
    return redirect('/youtube/reauthorize')
  }
  throw e
}

Prevention

When it happens

Trigger: The state was consumed/expired (Redis key TTL is 60*10 seconds = 10 minutes), the callback passes a userId different from the one that started authorization, state was altered/tampered in the redirect, Redis was flushed or restarted without persistence, or getAuthorizationUrl failed to store the key.

Common situations: User waits on the Google consent screen longer than 10 minutes then approves; user re-initiates OAuth in a second tab, overwriting/expiring the first state; callback routes to a different user session than the one that generated the URL; Redis restarts losing non-persisted keys.

Related errors


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

Appendix: source

Thrown at project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts:225

      // return response.data;  // 包含新的 access_token、expires_in、token_type 等信息
    } catch (err) {
      console.log('Error while refreshing access token', err);
      throw new Error('Failed to refresh access token');
    }
  }

  /**
   * 验证并保存授权码
   * @param code 授权码
   * @param state 状态码
   * @returns 系统令牌
   */
  async handleAuthorizationCode(code: string, state: string, userId: string) {
    try {
      // 获取state关联的邮箱信息
      const stateInfo = await this.redisService.get(`youtube:state:${userId}:${state}`);
      if (!stateInfo || !stateInfo.mail) {
        throw new Error('无效的状态码');
      }

      // 使用授权码获取访问令牌和刷新令牌
      const params = new URLSearchParams({
        code: code,
        redirect_uri: `${this.webRenderBaseUrl}/api/plat/youtube/auth/callback`,
        client_id: this.webClientId,
        grant_type: "authorization_code",
        client_secret: this.webClientSecret,
      });

      const response = await axios.post('https://oauth2.googleapis.com/token', params.toString(), {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
      });

      const { access_token, refresh_token, expires_in, id_token } = response.data;

      // 验证ID令牌以获取用户信息

View on GitHub (pinned to d3aa8bea5b)