gitbutlerapp/gitbutler · error · ApiError

Login token expired. Please log in to GitButler again.

Error message

Login token expired. Please log in to GitButler again.

What it means

ApiError thrown by parseResponseJSON in the shared HttpClient (packages/shared/src/lib/network/httpClient.ts:114) whenever any JSON request (get/post/put/patch/delete) receives HTTP 401. It means the X-Auth-Token header sent by HttpClient.request is missing, invalid, or expired, and the session must be re-established. The Response object is attached to the ApiError for status inspection.

Source

Thrown at packages/shared/src/lib/network/httpClient.ts:114

	async patch<T>(path: string, opts?: RequestOptions) {
		return await this.requestJson<T>(path, { ...opts, method: "PATCH" });
	}

	async delete<T>(path: string, opts?: RequestOptions) {
		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. Catch ApiError and check err.response.status === 401, then route the user to re-login
  2. Verify the token store feeding HttpClient still holds a token before authenticated calls
  3. Re-authenticate to mint a fresh token and retry the original request
  4. For long-lived sessions, refresh the token silently before it expires

Example fix

// before
const user = await httpClient.get("user");

// after
try {
	const user = await httpClient.get("user");
} catch (err) {
	if (err instanceof ApiError && err.response.status === 401) {
		session.clear(); // drop stale token and start the login flow
		return;
	}
	throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

import { get } from "svelte/store";

// run before any authenticated call
if (!get(httpClient.authenticationAvailable)) {
	// no token at all: go through the login flow instead of guaranteed 401s
	redirectToLogin();
}

Type guard

function isExpiredSessionError(err: unknown): err is ApiError {
	return err instanceof ApiError && err.response.status === 401;
}

Try / catch

try {
	await httpClient.get("user");
} catch (err) {
	if (isExpiredSessionError(err)) {
		await session.clearToken();   // drop the stale token
		redirectToLogin();            // re-authenticate, then retry the original request
	} else {
		throw err;
	}
}

Prevention

When it happens

Trigger: Any authenticated HttpClient call after the token TTL expires; the token store is empty so no X-Auth-Token header is sent; the session was revoked server-side (password change, logout on another device); token present but malformed.

Common situations: App left open past token expiry and resumed from sleep; token store cleared (logout elsewhere, storage eviction); environment pointed at an API that expects a different auth scheme; clock skew causing premature expiry.

Understand the failure class

Related errors


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