{"record":{"id":"7d40963b244955c9","repo":"tinyhumansai/openhuman","slug":"http-error-status-response-status","errorCode":null,"errorMessage":"HTTP error! status: ${response.status}","messagePattern":"HTTP error! status: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/apiClient.ts","lineNumber":99,"sourceCode":"    }\n\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n    const config: RequestInit = { method, headers, signal: controller.signal };\n\n    if (body && method !== 'GET') {\n      config.body = JSON.stringify(body);\n    }\n\n    try {\n      const response = await fetch(url, config);\n\n      // Handle non-JSON responses\n      const contentType = response.headers.get('content-type');\n      if (!contentType || !contentType.includes('application/json')) {\n        if (!response.ok) {\n          throw new Error(`HTTP error! status: ${response.status}`);\n        }\n        return {} as T;\n      }\n\n      const data = await response.json();\n\n      // Handle error responses\n      if (!response.ok) {\n        const error: ApiError = data.error\n          ? { success: false, error: data.error, message: data.message }\n          : { success: false, error: `HTTP ${response.status}: ${response.statusText}` };\n        throw error;\n      }\n\n      return data as T;\n    } catch (error) {\n      // Re-throw API errors as-is\n      if (error && typeof error === 'object' && 'error' in error) {","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/apiClient.ts#L81-L117","documentation":"Thrown by apiClient's request helper when the backend responds with a non-2xx status AND the response's content-type is not application/json — so the structured ApiError path (which parses data.error/data.message) is unreachable. The generic message embeds only the numeric status. This shape typically means an HTML/plain-text error page from a proxy, gateway, or wrong server rather than a well-behaved API error.","triggerScenarios":"backendUrl points at a domain served by nginx/a CDN returning an HTML 502/503/404 page; a reverse proxy blocking the route; requesting an API path from a host that serves a SPA (200 HTML would be returned as {} but error statuses hit this branch); auth portals (SSO/captive portal) answering HTML.","commonSituations":"Misconfigured BACKEND_API_URL (missing /api prefix, wrong protocol, trailing host without port); backend down behind a load balancer returning 502 HTML; corporate proxy/captive portal interception; WebSocket-only routes accidentally hit via this REST client.","solutions":["Check the embedded status: 502/503 → backend behind a proxy is down or unreachable; 404 → wrong base URL/path; 401/403 HTML → proxy/auth wall, not the API.","Open the same URL in a browser or curl -i to see the HTML/plain error — it usually names the gateway or proxy at fault.","Fix backendUrl (config_resolve_api_url / BACKEND_API_URL) to the real API origin.","If a proxy must sit in front, ensure it forwards the API routes and preserves JSON error content-type."],"exampleFix":"// before\nif (!contentType || !contentType.includes('application/json')) {\n  if (!response.ok) {\n    throw new Error(`HTTP error! status: ${response.status}`);\n  }\n  return {} as T;\n}\n\n// after — include a body snippet to identify proxy/HTML error pages\nif (!contentType || !contentType.includes('application/json')) {\n  if (!response.ok) {\n    const text = (await response.text().catch(() => '')).slice(0, 120);\n    throw new Error(`HTTP ${response.status} (non-JSON${text ? `: ${text}` : ''}) — is backendUrl correct?`);\n  }\n  return {} as T;\n}","handlingStrategy":"try-catch","validationCode":"const parsed = new URL(backendUrl);\nif (!/^https?:$/.test(parsed.protocol) || !parsed.hostname.includes('.')) {\n  throw new Error('backendUrl is not a valid API origin');\n}","typeGuard":"function isApiError(e: unknown): e is { success: false; error: string; message?: string } {\n  return Boolean(e) && typeof e === 'object' && (e as { success?: unknown }).success === false;\n}","tryCatchPattern":"try {\n  return await apiClient.request<T>(method, path, body);\n} catch (e) {\n  if (e instanceof Error && /^HTTP error! status: \\d+$/.test(e.message)) {\n    const status = Number(e.message.split(': ')[1]);\n    if (status >= 500 || status === 502) notify('Service unavailable — try again later.');\n    else if (status === 404) notify('API route not found — check backendUrl.');\n    else throw e;\n  } else if (isApiError(e)) {\n    notify(e.error);\n  } else throw e;\n}","preventionTips":["Validate backendUrl shape before the first request.","Non-JSON error bodies usually mean a proxy/gateway — inspect with curl -i when it appears.","Distinguish the ApiError object path from this generic string path when handling."],"tags":["http","backend-api","proxy","content-type","status-code"],"backgroundTag":"http-non-json-error-response","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}