Wei-Shaw/sub2api · error

未包含 refresh_token,且无法解析 accessToken 过期时间;请在第一步设置过期时间后再导入

Error message

未包含 refresh_token,且无法解析 accessToken 过期时间;请在第一步设置过期时间后再导入

What it means

Returned by resolveCodexImportExpiry (backend/internal/handler/admin/account_codex_import.go:805) when an import entry has no refresh_token AND no expiry can be determined — neither a parseable exp claim from the access token (item.TokenExpiresAt is nil) nor a request-level req.ExpiresAt. Without any expiry the scheduler cannot auto-stop the account, so the import is refused and the admin is told to set an expiry in step one of the import flow.

Source

Thrown at backend/internal/handler/admin/account_codex_import.go:805

		t := time.Unix(*req.ExpiresAt, 0).UTC()
		requestExpiresAt = &t
	}

	var accountExpiresAt *time.Time
	var credentialExpiresAt *time.Time
	warnings := make([]string, 0, 2)
	if item.RefreshToken == "" {
		if item.TokenExpiresAt != nil {
			tokenExpiresAt := item.TokenExpiresAt.UTC()
			accountExpiresAt = &tokenExpiresAt
			credentialExpiresAt = &tokenExpiresAt
		}
		if requestExpiresAt != nil {
			accountExpiresAt = earlierCodexTime(accountExpiresAt, requestExpiresAt)
			credentialExpiresAt = earlierCodexTime(credentialExpiresAt, requestExpiresAt)
		}
		if accountExpiresAt == nil {
			return nil, nil, nil, nil, errors.New("未包含 refresh_token,且无法解析 accessToken 过期时间;请在第一步设置过期时间后再导入")
		}
		if accountExpiresAt.Unix() <= time.Now().UTC().Unix()-codexImportClockSkewSeconds {
			return nil, nil, nil, nil, fmt.Errorf("过期时间已过期: %s", accountExpiresAt.Format(time.RFC3339))
		}
		warnings = append(warnings, "未包含 refresh_token,已按 accessToken/账号过期时间设置自动停止调度")
		if req.AutoPauseOnExpired != nil && !*req.AutoPauseOnExpired {
			warnings = append(warnings, "未包含 refresh_token,已强制开启过期自动暂停")
		}
		autoPause := true
		expiresAtUnix := accountExpiresAt.Unix()
		return &expiresAtUnix, credentialExpiresAt, &autoPause, warnings, nil
	}

	if requestExpiresAt != nil {
		accountExpiresAt = requestExpiresAt
	}
	if item.TokenExpiresAt != nil {
		tokenExpiresAt := item.TokenExpiresAt.UTC()

View on GitHub (pinned to 073e92d171)

Solutions

  1. Set ExpiresAt (unix seconds) on the import request to the token's known expiry, then re-import.
  2. Include a refresh_token in the entry so the account can renew and the expiry policy does not apply.
  3. If the access token is a JWT, verify it still carries a valid exp claim (decode it) — if parseable, TokenExpiresAt is populated automatically and this error disappears.
  4. If the token really has no refresh and unknown expiry, obtain a fresh token first.

Example fix

// before
POST /admin/codex/import
{ "items": [ { "accessToken": "opaque-token-no-exp" } ] }

// after
POST /admin/codex/import
{ "expiresAt": 1770000000, "items": [ { "accessToken": "opaque-token-no-exp" } ] }
Defensive patterns

Strategy: validation

Validate before calling

function canResolveExpiry(entry: { refresh_token?: string; accessToken?: string }, reqExpiresAt?: number): boolean {
  if (entry.refresh_token && entry.refresh_token.length > 0) return true
  if (typeof reqExpiresAt === 'number' && reqExpiresAt > 0) return true
  try {
    const payload = JSON.parse(atob(entry.accessToken!.split('.')[1]))
    return typeof payload.exp === 'number' && payload.exp > 0
  } catch { return false }
}

Prevention

When it happens

Trigger: Importing a Codex auth.json lacking refresh_token where the accessToken is opaque (exp not parseable) and the import request's ExpiresAt field is unset or zero. The nearby branch also rejects entries whose resolved expiry is already past ('过期时间已过期').

Common situations: Short-lived demo tokens pasted without metadata; JWTs signed with an unreadable/expired exp; admin skipping the 'set expiry' step in the two-step import wizard; exporting tokens from a tool that strips refresh tokens.

Related errors


AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15). Data as JSON: /api/errors/a6907a564b5b0bba. Report an issue: GitHub.