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
- Read the excerpt in the message — it usually contains the server's error code (e.g. invalid_grant, invalid_client)
- If the error is invalid_grant or expired token, re-authenticate from scratch (log in again) instead of refreshing
- Verify client credentials/base URL configuration matches what the auth provider expects
- 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
- Treat invalid_grant/invalid_client as terminal: force a fresh login instead of retrying
- Keep client credentials in sync with the provider's app configuration
- Log the response excerpt from the error to diagnose 4xx vs 5xx quickly
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
- await describeErrorResponse(response, fallback)
- Failed to fetch user info
- Could not reach : )` : ""}\n
- err.error_description || err.error || "Device token poll…
- ${err.error_description || err.error || "Device token poll…
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)