{"record":{"id":"437016c8ee24675b","repo":"jamiepine/voicebox","slug":"http-error-status-response-status","errorCode":null,"errorMessage":"HTTP error! status: ${response.status}","messagePattern":"HTTP error! status: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/lib/api/client.ts","lineNumber":93,"sourceCode":"    const serverUrl = useServerStore.getState().serverUrl;\n    return serverUrl;\n  }\n\n  private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {\n    const url = `${this.getBaseUrl()}${endpoint}`;\n    const response = await fetch(url, {\n      ...options,\n      headers: {\n        'Content-Type': 'application/json',\n        ...options?.headers,\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.json();\n  }\n\n  // Health\n  async getHealth(): Promise<HealthResponse> {\n    return this.request<HealthResponse>('/health');\n  }\n\n  // Profiles\n  async createProfile(data: VoiceProfileCreate): Promise<VoiceProfileResponse> {\n    return this.request<VoiceProfileResponse>('/profiles', {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/app/src/lib/api/client.ts#L75-L111","documentation":"Thrown by the shared `request<T>()` helper inside ApiClient (client.ts:79-97) whenever a JSON endpoint returns a non-2xx HTTP status. The error body is parsed as `{ detail }` (FastAPI convention); `formatErrorDetail` flattens string/array/object detail into a readable message, falling back to `HTTP error! status: <code>` when no detail is present. Because nearly every JSON method on the client routes through this helper, it is the single chokepoint for all REST failures (health, profiles, generations, history, effects, cloud).","triggerScenarios":"Any `this.request(...)` call — e.g. `getHealth()`, `createProfile()`, `listHistory()`, `getCloudStatus()` — when `getBaseUrl()` (read from `useServerStore.getState().serverUrl`) points at a backend that returns 4xx/5xx. Concretely: 404 when the endpoint path is wrong or the server is a different/older version; 422 when a FastAPI validation error occurs (detail is an array of `{msg,loc,type}`); 500 when the Python backend raises; ECONNREFUSED surfaces before this line as a fetch rejection, but a 502/504 from a proxy reaches here.","commonSituations":"Backend Voicebox Python server is not running or is on a different port than `serverUrl`; user changed the server URL in settings to a wrong value; backend version mismatch where the frontend expects an endpoint the server does not expose; FastAPI request-body validation failure on a create/update call; GPU/model not loaded causing a 500 during generation; CORS preflight failure manifests as an opaque error but a proxied 5xx reaches this throw.","solutions":["Confirm the backend is running and reachable: open `serverUrl + '/health'` in a browser or curl it; a 200 means the base URL is correct.","Read the thrown message — when `detail` is present it carries the FastAPI validation array joined by ';', which names the exact failing field. Fix the payload field it names.","If the message is the bare fallback `HTTP error! status: 404`, the endpoint does not exist on the backend — update the backend to match the client version or vice versa.","For 500s, check the backend process logs (the Python traceback identifies the server-side cause), not the frontend.","If `serverUrl` is wrong, correct it in the server settings UI which writes `useServerStore.serverUrl`."],"exampleFix":"// before — caller has no status context\nawait api.createProfile(data);\n\n// after — surface status code for diagnostics\ntry {\n  return await api.createProfile(data);\n} catch (e) {\n  const m = String(e?.message ?? e);\n  throw new Error(`createProfile failed: ${m}`, { cause: e });\n}","handlingStrategy":"try-catch","validationCode":"// Before any request(), confirm the backend is reachable\nasync function ensureBackend(baseUrl: string) {\n  const r = await fetch(`${baseUrl}/health`);\n  if (!r.ok) throw new Error(`Backend unreachable (health ${r.status})`);\n  return true;\n}","typeGuard":"// Narrow a thrown value to a usable message\nfunction isApiError(e: unknown): e is Error {\n  return e instanceof Error && /HTTP error! status: \\d+/.test(e.message);\n}\n\n// Extract the numeric status when present\nfunction httpStatusOf(e: unknown): number | null {\n  const m = String((e as Error)?.message ?? '').match(/status: (\\d+)/);\n  return m ? Number(m[1]) : null;\n}","tryCatchPattern":"try {\n  return await api.someMethod(payload);\n} catch (e) {\n  const status = httpStatusOf(e);\n  if (status === 422) showValidationToast((e as Error).message); // FastAPI detail array\n  else if (status && status >= 500) showRetryToast('Server error, please retry');\n  else throw e; // unknown — rethrow\n}","preventionTips":["Always validate request payloads client-side before calling to avoid 422 validation errors.","Confirm `useServerStore.serverUrl` points at a running backend before issuing calls (ping /health on settings change).","Keep the frontend and backend on matched versions so endpoint paths agree.","Surface status codes in error toasts so users can distinguish 404/422/500."],"tags":["http","api-client","network","fastapi","voicebox"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}