fix(ai): honor server-provided slow_down interval in device-code polling
GitHub's device flow documents slow_down as a rate limit: a poll that
arrives inside the throttle window is answered with slow_down instead of
the token, and the response's interval field reports the new required
minimum ("adds 5 seconds to the last interval"). A client that only
tracks its own +5s increment can stay behind the server's ratcheting
requirement when its timers fire early - common with WSL/VM clock drift
(microsoft/WSL#10006) - so every subsequent poll keeps hitting the rate
limit and login appears to hang forever even after the browser reports
the device as authorized.
Adopt the server-provided interval when a slow_down poll result carries
one, falling back to the RFC 8628 section 3.5 +5s increment otherwise.
GitHub Copilot passes the interval field through.
This restores part of the #1994 mitigations that were lost in the #4788
device-code refactor.
refs #6187
This commit is contained in:
@@ -10,7 +10,7 @@ const SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;
|
||||
|
||||
type OAuthDeviceCodeIncompletePollResult =
|
||||
| { status: "pending" }
|
||||
| { status: "slow_down" }
|
||||
| { status: "slow_down"; intervalSeconds?: number }
|
||||
| { status: "failed"; message: string };
|
||||
|
||||
export type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult | { status: "complete"; value: T };
|
||||
@@ -75,8 +75,15 @@ export async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodePollOpt
|
||||
}
|
||||
if (result.status === "slow_down") {
|
||||
slowDownResponses += 1;
|
||||
// RFC 8628 section 3.5: apply this increase to this and all subsequent requests.
|
||||
intervalMs = Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
|
||||
// Use the server-provided interval when given (GitHub reports the new required minimum
|
||||
// in `interval`); trusting only a client-tracked value risks polling early forever under
|
||||
// WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.
|
||||
intervalMs =
|
||||
typeof result.intervalSeconds === "number" &&
|
||||
Number.isFinite(result.intervalSeconds) &&
|
||||
result.intervalSeconds > 0
|
||||
? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))
|
||||
: Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
|
||||
}
|
||||
|
||||
const remainingMs = deadline - Date.now();
|
||||
|
||||
@@ -41,6 +41,7 @@ type DeviceTokenSuccessResponse = {
|
||||
type DeviceTokenErrorResponse = {
|
||||
error: string;
|
||||
error_description?: string;
|
||||
interval?: number;
|
||||
};
|
||||
|
||||
export function normalizeDomain(input: string): string | null {
|
||||
@@ -229,13 +230,13 @@ async function pollForGitHubAccessToken(
|
||||
}
|
||||
|
||||
if (raw && typeof raw === "object" && typeof (raw as DeviceTokenErrorResponse).error === "string") {
|
||||
const { error, error_description: description } = raw as DeviceTokenErrorResponse;
|
||||
const { error, error_description: description, interval } = raw as DeviceTokenErrorResponse;
|
||||
if (error === "authorization_pending") {
|
||||
return { status: "pending" };
|
||||
}
|
||||
|
||||
if (error === "slow_down") {
|
||||
return { status: "slow_down" };
|
||||
return { status: "slow_down", intervalSeconds: typeof interval === "number" ? interval : undefined };
|
||||
}
|
||||
|
||||
const descriptionSuffix = description ? `: ${description}` : "";
|
||||
|
||||
Reference in New Issue
Block a user