{"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/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/apiClient.ts#L81-L117","documentation":"Thrown by the generic fetch wrapper in apiClient when the response is NOT ok AND the content-type is not application/json — the two preconditions for trying to parse a structured ApiError. Because the body is not JSON, the status code is the only information available, hence the generic template `HTTP error! status: ${response.status}`.","triggerScenarios":"The cloud backend (or anything in front of it) returns an HTML error page with 502/503/504 (CDN/proxy like Cloudflare, maintenance page, gateway timeout), a plain-text 500 from an infra component, or the configured base URL points at a server that is not the API at all.","commonSituations":"Backend outage or deploy window surfaced as proxy HTML; wrong BACKEND/API base URL in app config or .env (hitting a marketing site or empty host); corporate TLS-inspection proxy replacing responses; backend returning empty bodies on 5xx.","solutions":["Read the status from the message: 502/503/504 are infra/proxy — retry shortly or check the backend status page; 404 usually means a wrong base URL","Verify the backend URL configuration (VITE_* config via app/src/utils/config.ts / .env files) points at the real API host","curl -i the same URL from the same machine and inspect what non-JSON body comes back and from which hop","Retry with backoff for 5xx gateway codes — these are typically transient","If a proxy strips JSON, fix the proxy/TLS-inspection exemption for the API host"],"exampleFix":"// before\nconst data = await apiClient.request<T>('/teams');\n\n// after\ntry {\n  const data = await apiClient.request<T>('/teams');\n} catch (e) {\n  if (e instanceof Error && /HTTP error! status: 5\\d\\d/.test(e.message)) {\n    return retryWithBackoff(() => apiClient.request<T>('/teams'));\n  }\n  throw e;\n}","handlingStrategy":"retry","validationCode":"// Pre-flight connectivity sanity: an infra/proxy error page usually fails a cheap probe too\nconst ok = await fetch(baseUrl, { method: 'HEAD' }).then(r => r.ok).catch(() => false);\nif (!ok) skipCloudCallAndWarn();","typeGuard":"function isNonJsonHttpError(e: unknown): e is Error {\n  return e instanceof Error && /HTTP error! status: \\d+/.test(e.message);\n}","tryCatchPattern":"try {\n  return await api.request<T>(path, init);\n} catch (e) {\n  const status = e instanceof Error ? Number(/status: (\\d+)/.exec(e.message)?.[1]) : 0;\n  if (status >= 500 && attempt < 3) {\n    await delay(2 ** attempt * 500);\n    return api.request<T>(path, init); // gateway errors are usually transient\n  }\n  if (status === 404) throw new Error(`API base URL may be wrong: ${baseUrl}`);\n  throw e;\n}","preventionTips":["Validate the backend base URL at startup with a HEAD/OPTIONS probe and fail with a clear message","Distinguish infra-status (5xx from proxies) from API-status (JSON {success:false}) in error handling","Watch for HTML content-types in monitoring — they betray proxy/TLS interference early"],"tags":["http","network","backend","proxy","fetch"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}