{"record":{"id":"c8f8d9171298ed14","repo":"upstash/context7","slug":"fallback-detail-excerpt","errorCode":null,"errorMessage":"${fallback} (${detail}): ${excerpt}","messagePattern":"\\$\\{fallback\\} \\(\\$\\{detail\\}\\): \\$\\{excerpt\\}","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/utils/auth.ts","lineNumber":228,"sourceCode":"  return `Could not reach ${url}: ${detail}${code ? ` (${code})` : \"\"}\\n${hint}`;\n}\n\nasync function postForm(url: string, params: URLSearchParams): Promise<Response> {\n  try {\n    return await fetch(url, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n      body: params.toString(),\n    });\n  } catch (error) {\n    throw new Error(describeConnectionError(error, url));\n  }\n}\n\nasync function oauthRequest<T>(url: string, params: URLSearchParams, fallback: string): Promise<T> {\n  const response = await postForm(url, params);\n  if (!response.ok) {\n    throw new Error(await describeErrorResponse(response, fallback));\n  }\n  return (await response.json()) as T;\n}\n\n/** RFC 8628 §3.2 default poll interval when the server omits `interval`. */\nexport const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;\n\nexport async function startDeviceAuthorization(\n  baseUrl: string,\n  clientId: string\n): Promise<DeviceAuthorizationResponse> {\n  // Hostname is shown on the server's verification page so the user can confirm\n  // that the device they're authorizing matches the one running the CLI\n  // (RFC 8628 §5.4 phishing resistance). Best-effort.\n  const params = new URLSearchParams({ client_id: clientId });\n  try {\n    const hostname = os.hostname();\n    if (hostname) params.set(\"hostname\", hostname);","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/upstash/context7/blob/4416fb855b8f752be735e34f943b5d0762701aad/packages/cli/src/utils/auth.ts#L210-L246","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: silently reusing a dead refresh token\nawait oauthRequest(tokenUrl, new URLSearchParams({ refresh_token: saved }), \"Token refresh failed\");\n// after: handle failure by falling back to full login\ntry {\n  tokens = await refreshAccessToken(saved);\n} catch {\n  tokens = await runDeviceAuthorizationFlow();\n}","handlingStrategy":"try-catch","validationCode":"// validate token looks present before refreshing\nif (!refreshToken || refreshToken.split(\".\").length !== 3) {\n  throw new Error(\"No valid refresh token stored; run login first\");\n}","typeGuard":"function isOAuthErrorResponse(body: unknown): body is { error: string; error_description?: string } {\n  return typeof body === \"object\" && body !== null && typeof (body as any).error === \"string\";\n}","tryCatchPattern":"try {\n  tokens = await refreshAccessToken(refreshToken);\n} catch (e) {\n  if (/invalid_grant|expired|revoked/i.test(e.message)) {\n    tokens = await runFreshLogin(); // re-authenticate\n  } else {\n    throw e; // transient/5xx — surface or retry\n  }\n}","preventionTips":["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"],"tags":["oauth","http","api","cli"],"backgroundTag":"http-error-response","analyzedSha":"4416fb855b8f752be735e34f943b5d0762701aad","analyzedAt":"2026-09-16T20:28:07.148Z","contentChangedAt":"2026-09-16T20:28:07.148Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}