upstash/context7 · error

( )

Error message

${fallback} (${detail}): ${excerpt}

What it means

Thrown by oauthRequest when the OAuth endpoint responded, but with a non-OK HTTP status. describeErrorResponse builds the message as `<fallback> (<detail>): <excerpt>` where fallback is the operation name (e.g. 'Token refresh failed'), detail is derived from the response, and excerpt is a truncated response body. This surfaces server-side rejections like 400 invalid_grant, 401 unauthorized_client, or 500s.

Solutions

  1. Read the excerpt in the message — it usually contains the server's error code (e.g. invalid_grant, invalid_client)
  2. If the error is invalid_grant or expired token, re-authenticate from scratch (log in again) instead of refreshing
  3. Verify client credentials/base URL configuration matches what the auth provider expects
  4. If the excerpt shows HTML or a 5xx, the server/proxy is failing — retry later or check provider status

Example fix

// before: silently reusing a dead refresh token
await oauthRequest(tokenUrl, new URLSearchParams({ refresh_token: saved }), "Token refresh failed");
// after: handle failure by falling back to full login
try {
  tokens = await refreshAccessToken(saved);
} catch {
  tokens = await runDeviceAuthorizationFlow();
}
Defensive patterns

Strategy: try-catch

Validate before calling

// validate token looks present before refreshing
if (!refreshToken || refreshToken.split(".").length !== 3) {
  throw new Error("No valid refresh token stored; run login first");
}

Type guard

function isOAuthErrorResponse(body: unknown): body is { error: string; error_description?: string } {
  return typeof body === "object" && body !== null && typeof (body as any).error === "string";
}

Try / catch

try {
  tokens = await refreshAccessToken(refreshToken);
} catch (e) {
  if (/invalid_grant|expired|revoked/i.test(e.message)) {
    tokens = await runFreshLogin(); // re-authenticate
  } else {
    throw e; // transient/5xx — surface or retry
  }
}

Prevention

When it happens

Trigger: Calling any oauthRequest-backed operation (refreshAccessToken, startDeviceAuthorization) where the server returns 4xx/5xx — expired/revoked refresh tokens, wrong client credentials, malformed request, or server error pages (HTML bodies from proxies).

Common situations: Refresh token revoked or expired after long inactivity, client ID/secret mismatch after config change, SSO session invalidated on the provider side, or an API gateway returning 502/503 HTML instead of JSON.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of upstash/context7@4416fb855b (2026-09-16). Data as JSON: /api/errors/c8f8d9171298ed14. Report an issue: GitHub.

Appendix: source

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

  return `Could not reach ${url}: ${detail}${code ? ` (${code})` : ""}\n${hint}`;
}

async function postForm(url: string, params: URLSearchParams): Promise<Response> {
  try {
    return await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: params.toString(),
    });
  } catch (error) {
    throw new Error(describeConnectionError(error, url));
  }
}

async function oauthRequest<T>(url: string, params: URLSearchParams, fallback: string): Promise<T> {
  const response = await postForm(url, params);
  if (!response.ok) {
    throw new Error(await describeErrorResponse(response, fallback));
  }
  return (await response.json()) as T;
}

/** RFC 8628 §3.2 default poll interval when the server omits `interval`. */
export const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;

export async function startDeviceAuthorization(
  baseUrl: string,
  clientId: string
): Promise<DeviceAuthorizationResponse> {
  // Hostname is shown on the server's verification page so the user can confirm
  // that the device they're authorizing matches the one running the CLI
  // (RFC 8628 §5.4 phishing resistance). Best-effort.
  const params = new URLSearchParams({ client_id: clientId });
  try {
    const hostname = os.hostname();
    if (hostname) params.set("hostname", hostname);

View on GitHub (pinned to 4416fb855b)