NES 402 handler resets the Copilot token in an unbounded loop (quota latch never engages)
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
The 402 handler in `completionsFetchServiceImpl.ts` is meant to refresh the token once so a quota latch flips and suppresses further resets. The latch never engages, so every 402 triggers a token reset — indefinitely.
```ts
// extensions/copilot/src/platform/nesFetch/node/completionsFetchServiceImpl.ts
if (response.status === 402) {
if (!this.authService.copilotToken?.isCompletionsQuotaExceeded) {
this.authService.resetCopilotToken(response.status);
await this.authService.getCopilotToken();
}
}
```
**Root cause.** The guard reads the wrong quota bucket:
```ts
// extensions/copilot/src/platform/authentication/common/copilotToken.ts
get isCompletionsQuotaExceeded(): boolean {
return this.isFreeUser && (this._info.limited_user_quotas?.completions ?? 1) <= 0;
}
```
`limited_user_quotas.completions` is free-tier **code-completions** quota, but the 402 originates from **NES**. A user can be out of NES allowance while `completions` stays > 0 — or the field is absent, where `?? 1` defaults to "quota available". The condition is unsatisfiable for this 402, so the re-mint is a no-op and the next request repeats it.
Secondary: the `isFreeUser &&` prefix makes the latch unconditionally false for any paid user. Latent while 402s are free-tier, but it activates as soon as paid users hit NES 402s.
`chatMLFetcher.ts` has the same shape via `isChatQuotaExceeded` (HTTP 402 branch and the CAPI `quota_exceeded` / `free_quota_exceeded` / `overage_limit_reached` / `billing_not_configured` / `additional_spend_limit_reached` branch).
**Observed behavior.** Measured over 24h in a downstream consumer of this code (GitHub Copilot Language Server, which ships it via `@vscode/chat-lib`), across free-tier users hitting NES 402s — share of users by how many token resets they performed:
| Resets per user per day | Share of affected users |
| --- | --- |
| 1 (the intended one-shot) | 2% |
| 2–10 | 5% |
| more than 100 | 78% |
| more than 1000 | 27% |
Only ~2% of affected users follow the intended path. For the rest the re-mint provably never changes the state the guard tests, so the reset repeats at request rate — we've observed sustained per-minute reset rates on individual devices. The affected population has been growing week over week.
**Impact.** Repeated token minting from clients that cannot benefit, plus wasted CPU and network on affected machines. The same code path runs in the VS Code Copilot extension, so the spin may occur there too; we have no visibility into that population to confirm.
**Suggested fix.**
1. Latch on the quota that produced the 402 (or on the 402 itself / `retry-after`), not `limited_user_quotas.completions`.
2. Drop the `isFreeUser &&` prefix so paid users are covered.
3. Add a cap/backoff so a non-latching guard cannot spin at request rate.
**Source.** Confirmed present on `main` today in `extensions/copilot/src/platform/nesFetch/node/completionsFetchServiceImpl.ts` and `extensions/copilot/src/platform/authentication/common/copilotToken.ts`. Also reproduced via `@vscode/chat-lib` 0.60.0, whose published `repository` field still points at the now-archived `microsoft/vscode-copilot-chat`.
Contributor guide
Assessment
This issue has not been assessed yet.