gitbutlerapp/gitbutler · error · ApiError
HTTP Error ${response.statusText}: ${text}
Error message
HTTP Error ${response.statusText}: ${text} What it means
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.
Source
Thrown at packages/shared/src/lib/network/httpClient.ts:120
return await this.requestJson<T>(path, { ...opts, method: "DELETE" });
}
async postRaw(path: string, opts?: RequestOptions) {
return await this.request(path, { ...opts, method: "POST" });
}
}
async function parseResponseJSON(response: Response) {
if (response.status === 204 || response.status === 205) {
return null;
} else if (response.status === 401) {
throw new ApiError("Login token expired. Please log in to GitButler again.", response);
} else if (response.status >= 400) {
const text = await response.text();
if (text.includes("401 Unauthorized") || text.includes("401 unauthorized")) {
throw new ApiError("Login token expired. Please log in to GitButler again.", response);
}
throw new ApiError(`HTTP Error ${response.statusText}: ${text}`, response);
} else {
return await response.json();
}
}
function formatBody(body?: FormData | object) {
if (!body) return;
return body instanceof FormData ? body : JSON.stringify(body);
}
View on GitHub (pinned to 2497b8007a)
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
Example fix
// before
const data = await httpClient.get("user/profile");
// after
try {
const data = await httpClient.get("user/profile");
} catch (err) {
if (err instanceof ApiError) {
const { status } = err.response;
if (status === 429 || status >= 500) return retryLater(err);
throw new Error(`Profile request failed (${status}): ${err.message}`);
}
throw err;
} Defensive patterns
Strategy: try-catch
Type guard
function isApiError(err: unknown): err is ApiError {
return err instanceof ApiError;
} Try / catch
try {
const data = await httpClient.get("things");
} catch (err) {
if (!isApiError(err)) throw err;
const { status } = err.response;
if (status === 429 || status >= 500) return retryWithBackoff(); // transient
if (status >= 400) throw new ValidationError(err.message); // caller bug: fix request
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Login token expired. Please log in to GitButler again.
- No commits were provided to squash
- no commit IDs provided for uncommit
- Branch {branch_name} not found
- Failed to create pull request: {status} - {error_text}
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/88dc11f53f8edadf.
Report an issue: GitHub.