decolua/9router · error · Error

data.errorDescription || data.error

Error message

data.errorDescription || data.error

What it means

During the device-code polling loop in OAuthModal, when the poll response carries error === "expired_token" or "access_denied", the modal throws an Error preferring the provider-supplied `errorDescription`, falling back to the bare error code. This ends polling with the error step. `slow_down` and `authorization_pending` are handled separately (backoff / keep polling) and do not reach this throw.

Source

Thrown at src/shared/components/OAuthModal.js:163

      try {
        const res = await fetch(`/api/oauth/${provider}/poll`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ deviceCode, codeVerifier, extraData }),
        });

        const data = await res.json();

        if (data.success) {
          pollingAbortRef.current = true; // Stop polling immediately
          setStep("success");
          setPolling(false);
          onSuccess?.();
          return;
        }

        if (data.error === "expired_token" || data.error === "access_denied") {
          throw new Error(data.errorDescription || data.error);
        }

        if (data.error === "slow_down") {
          interval = Math.min(interval + 5, 30);
        }
      } catch (err) {
        setError(err.message);
        setStep("error");
        setPolling(false);
        return;
      }
    }

    setError("Authorization timeout");
    setStep("error");
    setPolling(false);
  }, [provider, onSuccess]);

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Restart the device flow to get a new device/user code and approve it promptly.
  2. If access_denied, re-run the flow and approve the consent screen (check with your admin if SSO policy blocks it).
  3. Read the errorDescription in the thrown message — it states whether the code expired or consent was denied.
  4. For repeated expired_token, reduce friction: have the auth page open and ready before starting, and don't pause/hibernate the machine mid-flow.

Example fix

// before
if (data.error === "expired_token" || data.error === "access_denied") {
  throw new Error(data.errorDescription || data.error);
}
// after
if (data.error === "expired_token" || data.error === "access_denied") {
  const reason = data.error === "expired_token"
    ? "Device code expired — restart the sign-in and approve within the time limit"
    : "Authorization was denied — restart the sign-in and approve the consent request";
  throw new Error(data.errorDescription || reason);
}
Defensive patterns

Strategy: retry

Validate before calling

// only treat terminal errors as fatal; keep polling pending/slow_down
const TERMINAL = ["expired_token", "access_denied"];
if (TERMINAL.includes(data.error)) {
  // stop polling and prompt a fresh device-code request
  setPolling(false);
  setError(data.errorDescription || data.error);
  return;
}

Try / catch

try {
  await pollDeviceToken();
} catch (err) {
  if (err.message.includes("expired")) {
    // auto-restart the device flow once
    await startDeviceFlow();
  } else {
    setError(err.message);
  }
}

Prevention

When it happens

Trigger: The device-flow poll endpoint returns { error: "expired_token" } — the device code outlived its validity because the user took too long to authorize — or { error: "access_denied" } — the user explicitly denied the consent request at the provider.

Common situations: User leaves the device-authorization page open but doesn't approve before the code expires (typically 5–15 min); user clicks 'Deny'/'Cancel' on the consent screen; interval backoff mishandled causing provider-side expiry; workplace SSO policy auto-denies the request.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/0679245971e41e0f. Report an issue: GitHub.