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

  1. Read err.response.status plus the body text embedded in the message — together they identify the failing endpoint and reason
  2. Fix the request (path, payload, headers) for 4xx responses; retry only 429/5xx with backoff
  3. Check the API status page or server logs if 5xx responses cluster
  4. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/88dc11f53f8edadf. Report an issue: GitHub.