{"record":{"id":"46a7691a8c0a54a4","repo":"can1357/oh-my-pi","slug":"http-error-res-status-on-endpoint","errorCode":null,"errorMessage":"HTTP error ${res.status} on ${endpoint}","messagePattern":"HTTP error (.+?) on (.+?)","errorType":"exception","errorClass":"ApiError","httpStatus":null,"severity":"error","filePath":"packages/stats/src/client/api.ts","lineNumber":32,"sourceCode":"\nconst API_BASE = \"/api\";\n\nexport class ApiError extends Error {\n\tstatus: number;\n\tendpoint: string;\n\n\tconstructor(status: number, endpoint: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"ApiError\";\n\t\tthis.status = status;\n\t\tthis.endpoint = endpoint;\n\t}\n}\n\nasync function fetchJson<T>(endpoint: string, options?: RequestInit): Promise<T> {\n\tconst res = await fetch(endpoint, options);\n\tif (!res.ok) {\n\t\tthrow new ApiError(res.status, endpoint, `HTTP error ${res.status} on ${endpoint}`);\n\t}\n\treturn res.json() as Promise<T>;\n}\n\nexport async function getOverviewStats(range: TimeRange = \"24h\", signal?: AbortSignal): Promise<OverviewStats> {\n\treturn fetchJson<OverviewStats>(`${API_BASE}/stats/overview?range=${encodeURIComponent(range)}`, {\n\t\tsignal,\n\t});\n}\n\nexport async function getModelDashboardStats(\n\trange: TimeRange = \"24h\",\n\tsignal?: AbortSignal,\n): Promise<ModelDashboardStats> {\n\treturn fetchJson<ModelDashboardStats>(`${API_BASE}/stats/model-dashboard?range=${encodeURIComponent(range)}`, {\n\t\tsignal,\n\t});\n}","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/stats/src/client/api.ts#L14-L50","documentation":"fetchJson() in the omp stats dashboard client wraps every REST call and throws ApiError when the HTTP response status is not ok (anything outside 2xx). The error carries the status code and the endpoint so the UI layer can distinguish server failures from bad requests. It applies to all stats fetches: overview, dashboards, recent requests/errors, and request details.","triggerScenarios":"Any exported fetcher (getOverviewStats, getModelDashboardStats, getCostDashboardStats, getRecentRequests, getRecentErrors, getRequestDetails) receiving a 4xx/5xx response — server not running or crashed, wrong API_BASE, range parameter rejected, endpoint changed between versions, or a proxy returning 404/502.","commonSituations":"The omp stats server was restarted or is still booting while the dashboard polls; hitting an old cached bundle pointed at a removed endpoint; a reverse proxy answering 502 because the upstream stats server died; invalid TimeRange value producing a 400.","solutions":["Check the stats server is running and reachable at API_BASE (curl the endpoint to see the real status/body)","Retry with backoff if the server was booting (transient 502/503)","Verify API_BASE and endpoint paths match the running server version","Inspect the ApiError.status to branch: 404 → version mismatch/wrong base URL; 5xx → server-side failure; 4xx → bad parameters"],"exampleFix":"// before\nconst stats = await getOverviewStats(range); // throws ApiError on 500\n// after\ntry {\n  const stats = await getOverviewStats(range);\n} catch (err) {\n  if (err instanceof ApiError && (err.status === 502 || err.status === 503)) {\n    stats = await withRetry(() => getOverviewStats(range));\n  } else throw err;\n}","handlingStrategy":"retry","validationCode":"const res = await fetch(`${API_BASE}/stats/overview?range=${range}`, { method: 'HEAD' });\nif (!res.ok) console.warn(`Stats API unhealthy: ${res.status}`);","typeGuard":"function isApiError(err: unknown): err is ApiError {\n  return err instanceof ApiError;\n}","tryCatchPattern":"try {\n  const stats = await getOverviewStats(range, signal);\n} catch (err) {\n  if (err instanceof ApiError && err.status >= 500) {\n    // transient — retry with backoff\n  } else if (err instanceof ApiError && err.status === 404) {\n    // version/base-URL mismatch — surface a config hint\n  } else throw err;\n}","preventionTips":["Health-check the stats server before polling the dashboard endpoints","Branch on ApiError.status rather than treating all failures alike","Keep API_BASE and the client bundle versions in sync with the server","Pass an AbortSignal so stale requests do not pile up during outages"],"tags":["http","network","api","fetch"],"backgroundTag":"http-error-status","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}