upstash/context7 · error · Error

Device token poll failed

Error message

Device token poll failed

What it means

Thrown in the default branch of pollDeviceToken()'s switch after a non-2xx, non-5xx response. Recognized OAuth device-code errors (authorization_pending, slow_down, access_denied, expired_token) return a structured status; any other error code falls through and throws err.error_description || err.error || 'Device token poll failed'.

Source

Thrown at packages/cli/src/utils/auth.ts:305

    const err = (await response.json().catch(() => ({}))) as TokenErrorResponse;
    return {
      status: "transient",
      errorMessage: err.error_description || err.error || `HTTP ${response.status}`,
    };
  }

  const err = (await response.json().catch(() => ({}))) as TokenErrorResponse;
  switch (err.error) {
    case "authorization_pending":
      return { status: "pending" };
    case "slow_down":
      return { status: "slow_down" };
    case "access_denied":
      return { status: "denied" };
    case "expired_token":
      return { status: "expired" };
    default:
      throw new Error(err.error_description || err.error || "Device token poll failed");
  }
}

View on GitHub (pinned to ca15df0443)

Solutions

  1. Restart the device flow from scratch (new device_code) rather than resuming the poll.
  2. Update the CLI — newer versions may recognize the new error code.
  3. Inspect err.error in the thrown message; an unrecognized value usually means server/client contract drift.
  4. If err.error is undefined, the body was not JSON — check for an intercepting gateway and verify the auth host.

Example fix

// before — only the fallback string reaches the user
default:
  throw new Error(err.error_description || err.error || "Device token poll failed");

// after — surface the raw error code so unrecognized codes are debuggable
const code = err.error || "unknown";
throw new Error(err.error_description || err.error || `Device token poll failed (error=${code})`);
Defensive patterns

Strategy: validation

Validate before calling

// Only poll while the flow is plausibly live; stop after a max attempt budget.
const deadline = Date.now() + 5 * 60_000;
let attempt = 0;
while (Date.now() < deadline && attempt++ < 60) {
  const r = await pollDeviceToken(baseUrl, clientId, deviceCode);
  if (r.status === "approved") return r.tokens!;
  if (r.status === "denied" || r.status === "expired") break;
  await sleep((r.status === "slow_down" ? 10 : 5) * 1000);
}
throw new Error("Device authorization timed out or ended without approval");

Type guard

// Narrow the parsed token-error body.
function isTokenError(v: unknown): v is { error: string; error_description?: string } {
  return typeof v === "object" && v !== null && typeof (v as any).error === "string";
}

Try / catch

try {
  await pollDeviceToken(baseUrl, clientId, deviceCode);
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  // An unrecognized code usually means contract drift or a stale device_code;
  // restart the flow rather than resuming.
  if (/invalid_grant|invalid_client|unsupported_grant_type/.test(msg)) {
    return restartDeviceFlow();
  }
  throw e;
}

Prevention

When it happens

Trigger: OAuth server returns invalid_grant (device_code reused/expired unexpectedly), invalid_client, unsupported_grant_type, a new/proprietary error code, or a non-JSON body so err is {} and both err.error and err.error_description are undefined.

Common situations: Polling continued past token issuance causing a second poll with an already-consumed device_code; client_id rotated mid-flow; auth server upgraded and now emits an error code the CLI version does not know about; CDN/gateway replaced the body with HTML so the JSON parse silently yields {}.

Related errors


AI-assisted analysis of upstash/context7@ca15df0443 (2026-08-12). Data as JSON: /api/errors/e03f00439b5c4e2f. Report an issue: GitHub.