{"record":{"id":"88dc11f53f8edadf","repo":"gitbutlerapp/gitbutler","slug":"http-error-response-statustext-text","errorCode":null,"errorMessage":"HTTP Error ${response.statusText}: ${text}","messagePattern":"HTTP Error (.+?): (.+?)","errorType":"http","errorClass":"ApiError","httpStatus":null,"severity":"error","filePath":"packages/shared/src/lib/network/httpClient.ts","lineNumber":120,"sourceCode":"\t\treturn await this.requestJson<T>(path, { ...opts, method: \"DELETE\" });\n\t}\n\n\tasync postRaw(path: string, opts?: RequestOptions) {\n\t\treturn await this.request(path, { ...opts, method: \"POST\" });\n\t}\n}\n\nasync function parseResponseJSON(response: Response) {\n\tif (response.status === 204 || response.status === 205) {\n\t\treturn null;\n\t} else if (response.status === 401) {\n\t\tthrow new ApiError(\"Login token expired. Please log in to GitButler again.\", response);\n\t} else if (response.status >= 400) {\n\t\tconst text = await response.text();\n\t\tif (text.includes(\"401 Unauthorized\") || text.includes(\"401 unauthorized\")) {\n\t\t\tthrow new ApiError(\"Login token expired. Please log in to GitButler again.\", response);\n\t\t}\n\t\tthrow new ApiError(`HTTP Error ${response.statusText}: ${text}`, response);\n\t} else {\n\t\treturn await response.json();\n\t}\n}\n\nfunction formatBody(body?: FormData | object) {\n\tif (!body) return;\n\treturn body instanceof FormData ? body : JSON.stringify(body);\n}\n","sourceCodeStart":102,"sourceCodeEnd":130,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/packages/shared/src/lib/network/httpClient.ts#L102-L130","documentation":"Fallback ApiError from parseResponseJSON (httpClient.ts:120) for any status >= 400 that is not a 401 and whose body does not contain \"401 Unauthorized\". The message interpolates response.statusText and the raw body text, so the string varies with the server response (HTTP/2 responses often leave statusText empty). The Response object is attached for programmatic status handling.","triggerScenarios":"400 validation rejections, 403 forbidden, 404 from a wrong path, 409 conflicts, 429 rate limits, and 500/502/503 server or gateway failures on any HttpClient get/post/put/patch/delete call.","commonSituations":"Wrong endpoint path or API version; expired CSRF/session cookies on non-token endpoints; rate limiting; backend deployment outages; malformed request payloads rejected by validation.","solutions":["Read err.response.status plus the body text embedded in the message — together they identify the failing endpoint and reason","Fix the request (path, payload, headers) for 4xx responses; retry only 429/5xx with backoff","Check the API status page or server logs if 5xx responses cluster","Do not string-match the message; branch on err.response.status so empty statusText (HTTP/2) does not break handling"],"exampleFix":"// before\nconst data = await httpClient.get(\"user/profile\");\n\n// after\ntry {\n\tconst data = await httpClient.get(\"user/profile\");\n} catch (err) {\n\tif (err instanceof ApiError) {\n\t\tconst { status } = err.response;\n\t\tif (status === 429 || status >= 500) return retryLater(err);\n\t\tthrow new Error(`Profile request failed (${status}): ${err.message}`);\n\t}\n\tthrow err;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isApiError(err: unknown): err is ApiError {\n\treturn err instanceof ApiError;\n}","tryCatchPattern":"try {\n\tconst data = await httpClient.get(\"things\");\n} catch (err) {\n\tif (!isApiError(err)) throw err;\n\tconst { status } = err.response;\n\tif (status === 429 || status >= 500) return retryWithBackoff(); // transient\n\tif (status >= 400) throw new ValidationError(err.message);        // caller bug: fix request\n}","preventionTips":["Validate request payloads and paths before sending to avoid 400/404","Retry only 429/5xx with backoff; never retry 4xx blindly","Branch on err.response.status instead of parsing the interpolated message","Remember statusText is often empty on HTTP/2 — rely on the numeric status"],"tags":["http","api","network","error-handling"],"backgroundTag":"http-error-response","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}