{"record":{"id":"cd3131b52e45aa27","repo":"gitbutlerapp/gitbutler","slug":"login-token-expired-please-log-in-to-gitbutler-ag","errorCode":null,"errorMessage":"Login token expired. Please log in to GitButler again.","messagePattern":"Login token expired\\. Please log in to GitButler again\\.","errorType":"http","errorClass":"ApiError","httpStatus":401,"severity":"error","filePath":"packages/shared/src/lib/network/httpClient.ts","lineNumber":114,"sourceCode":"\n\tasync patch<T>(path: string, opts?: RequestOptions) {\n\t\treturn await this.requestJson<T>(path, { ...opts, method: \"PATCH\" });\n\t}\n\n\tasync delete<T>(path: string, opts?: RequestOptions) {\n\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":96,"sourceCodeEnd":130,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/packages/shared/src/lib/network/httpClient.ts#L96-L130","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Catch ApiError and check err.response.status === 401, then route the user to re-login","Verify the token store feeding HttpClient still holds a token before authenticated calls","Re-authenticate to mint a fresh token and retry the original request","For long-lived sessions, refresh the token silently before it expires"],"exampleFix":"// before\nconst user = await httpClient.get(\"user\");\n\n// after\ntry {\n\tconst user = await httpClient.get(\"user\");\n} catch (err) {\n\tif (err instanceof ApiError && err.response.status === 401) {\n\t\tsession.clear(); // drop stale token and start the login flow\n\t\treturn;\n\t}\n\tthrow err;\n}","handlingStrategy":"try-catch","validationCode":"import { get } from \"svelte/store\";\n\n// run before any authenticated call\nif (!get(httpClient.authenticationAvailable)) {\n\t// no token at all: go through the login flow instead of guaranteed 401s\n\tredirectToLogin();\n}","typeGuard":"function isExpiredSessionError(err: unknown): err is ApiError {\n\treturn err instanceof ApiError && err.response.status === 401;\n}","tryCatchPattern":"try {\n\tawait httpClient.get(\"user\");\n} catch (err) {\n\tif (isExpiredSessionError(err)) {\n\t\tawait session.clearToken();   // drop the stale token\n\t\tredirectToLogin();            // re-authenticate, then retry the original request\n\t} else {\n\t\tthrow err;\n\t}\n}","preventionTips":["Check get(httpClient.authenticationAvailable) before authenticated calls","Refresh or re-issue the session token before its TTL expires","Clear the token store on logout in every tab to avoid revoked-token 401s","Centralize the 401 handler once instead of catching per call site"],"tags":["authentication","http-401","token-expiry","network"],"backgroundTag":"auth-token-expired","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}