{"record":{"id":"760f771d28bc6a6b","repo":"abhigyanpatwari/GitNexus","slug":"message","errorCode":null,"errorMessage":"${message}","messagePattern":"\\$\\{message\\}","errorType":"http","errorClass":"BackendError","httpStatus":null,"severity":"error","filePath":"gitnexus-web/src/services/backend-client.ts","lineNumber":560,"sourceCode":"  // express-rate-limit emits it on 429 with seconds (integer) or HTTP-date.\n  // We accept both shapes; an unparseable header yields undefined retryAfterMs.\n  let retryAfterMs: number | undefined;\n  if (response.status === 429) {\n    const header = response.headers.get('retry-after');\n    if (header) {\n      const seconds = Number(header);\n      if (Number.isFinite(seconds) && seconds >= 0) {\n        retryAfterMs = seconds * 1000;\n      } else {\n        const dateMs = Date.parse(header);\n        if (Number.isFinite(dateMs)) {\n          retryAfterMs = Math.max(0, dateMs - Date.now());\n        }\n      }\n    }\n  }\n\n  throw new BackendError(message, response.status, code, retryAfterMs);\n};\n\nconst repoParam = (repo?: string): string => (repo ? `repo=${encodeURIComponent(repo)}` : '');\n\n// ── API Methods ────────────────────────────────────────────────────────────\n\n/** Server info from /api/info. */\nexport interface ServerInfo {\n  version: string;\n  launchContext: 'npx' | 'global' | 'local';\n  nodeVersion: string;\n}\n\n/** Fetch server info (version, launch context). */\nexport const fetchServerInfo = async (): Promise<ServerInfo> => {\n  const response = await fetchWithTimeout(`${_backendUrl}/api/info`);\n  await assertOk(response);\n  return response.json() as Promise<ServerInfo>;","sourceCodeStart":542,"sourceCodeEnd":578,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/gitnexus-web/src/services/backend-client.ts#L542-L578","documentation":"BackendError thrown by assertOk() for any non-OK HTTP response that doesn't match a more specific code path. The message is derived from the response body's `error` or `message` field (falling back to statusText); the code is computed from status: 404→'not_found', 429→'rate_limited', 401+unauthorized→'unauthorized', 403+origin_not_allowed→'origin_blocked', other 4xx→'client', 5xx→'server'. For 429, retryAfterMs is parsed from the Retry-After header (delta-seconds or HTTP-date).","triggerScenarios":"Any fetch returning response.ok === false: a 400 (bad request body), 401 (auth), 403 (forbidden/origin blocked), 404 (not found), 409 (conflict), 429 (rate limited), 500/502/503 (server error). assertOk parses the JSON body for an error/message/code field and constructs the appropriate BackendError.","commonSituations":"404 when the repo isn't indexed; 401 when the deploy access token is missing/wrong on a gated deploy; 403 origin_not_allowed when hitting a write route from a different host; 429 when express-rate-limit trips; 500 when the backend hits an internal error.","solutions":["Inspect error.status and error.code to determine the category (client/server/not_found/unauthorized/etc.)","For code 'unauthorized', prompt the user for the deploy access token and retry","For code 'origin_blocked', open the local UI on the same host as the backend","For code 'rate_limited', honor error.retryAfterMs before retrying","For 5xx 'server', check the backend logs — it's a server-side fault"],"exampleFix":"// before — generic catch, no branching\ntry { await runQuery(cypher); }\ncatch (e) { console.error(e.message); }\n\n// after — branch on BackendError code\ntry { await runQuery(cypher); }\ncatch (e) {\n  if (e instanceof BackendError) {\n    if (e.code === 'unauthorized') promptForToken();\n    else if (e.code === 'rate_limited') scheduleRetry(e.retryAfterMs);\n    else showServerError(e.message, e.status);\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight: probe auth before issuing the real request on a gated deploy\nimport { fetchServerInfo, BackendError } from './services/backend-client.js';\ntry { await fetchServerInfo(); }\ncatch (e) {\n  if (e instanceof BackendError && e.code === 'unauthorized') promptForToken();\n}","typeGuard":"import { BackendError } from './services/backend-client.js';\nfunction isBackendError(e: unknown): e is BackendError {\n  return e instanceof BackendError;\n}\n// Narrow by code:\nfunction isNotFound(e: BackendError): boolean { return e.code === 'not_found'; }\nfunction isUnauthorized(e: BackendError): boolean { return e.code === 'unauthorized'; }\nfunction isRateLimited(e: BackendError): boolean { return e.code === 'rate_limited'; }","tryCatchPattern":"try {\n  await fetchData();\n} catch (e) {\n  if (e instanceof BackendError) {\n    switch (e.code) {\n      case 'unauthorized': promptForToken(); break;\n      case 'origin_blocked': openLocalUi(); break;\n      case 'rate_limited': scheduleRetry(e.retryAfterMs); break;\n      case 'not_found': showNotFound(e.message); break;\n      case 'client': showClientError(e.message, e.status); break;\n      case 'server': showServerError(e.message, e.status); break;\n    }\n  } else throw e;\n}","preventionTips":["Always branch on BackendError.code rather than parsing the message — codes are stable, messages aren't","For 429, honor e.retryAfterMs (parsed from Retry-After) before retrying","For 401 unauthorized, prompt for the deploy access token; for 403 origin_blocked, open the local UI","Inspect e.status for HTTP-specific handling (e.g. 404 vs 409)"],"tags":["http","backend","error-handling","api"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}