mastra-ai/mastra · error
${body?.error} or Failed to save GitHub token (${res.status}
Error message
${body?.error} or Failed to save GitHub token (${res.status}) What it means
saveGithubPat POSTs a GitHub token to `${baseUrl}/web/github/pat`. When the response is not ok, it first tries to read a JSON error body and rethrow the server-provided `error` message; if the body is absent or unparseable, it throws this generic fallback including the HTTP status. The library throws it so callers get a single Error instead of silently treating a failed save as success.
Source
Thrown at mastracode/factory-ui/src/ui/domains/workspaces/services/github.ts:172
const res = await fetch(`${baseUrl}/web/github/pat`, {
headers: { Accept: 'application/json' },
credentials: 'include',
});
if (!res.ok) throw new Error(`Failed to load GitHub token status (${res.status})`);
return (await res.json()) as GithubPatStatus;
}
/** Save an org GitHub PAT (used only for `gh` CLI auth in sandboxes). */
export async function saveGithubPat(baseUrl: string, token: string, kind: GithubPatKind = 'default'): Promise<void> {
const res = await fetch(`${baseUrl}/web/github/pat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
credentials: 'include',
body: JSON.stringify({ token, kind }),
});
if (!res.ok) {
const body = (await res.json().catch(() => undefined)) as { error?: string } | undefined;
throw new Error(body?.error ?? `Failed to save GitHub token (${res.status})`);
}
}
/** Remove an org GitHub PAT. */
export async function deleteGithubPat(baseUrl: string, kind: GithubPatKind = 'default'): Promise<void> {
const res = await fetch(`${baseUrl}/web/github/pat?kind=${kind}`, {
method: 'DELETE',
headers: { Accept: 'application/json' },
credentials: 'include',
});
if (!res.ok) throw new Error(`Failed to remove GitHub token (${res.status})`);
}
/** List repos across the user's installations, optionally filtered by query. */
export async function listGithubRepos(baseUrl: string, query?: string): Promise<GithubRepo[]> {
const url = query ? `${baseUrl}/web/github/repos?q=${encodeURIComponent(query)}` : `${baseUrl}/web/github/repos`;
const res = await fetch(url, { headers: { Accept: 'application/json' }, credentials: 'include' });
if (!res.ok) throw new Error(`Failed to list repos (${res.status})`);View on GitHub (pinned to 75dd419e61)
Solutions
- Read the thrown message: if it is the server's `error` string, fix what the server says (usually invalid/expired token) and retry with a fresh PAT
- Check the numeric status in the fallback message; 401/403 means re-authenticate or verify workspace permissions before retrying
- Verify the user is logged in and baseUrl targets the correct server origin so the `credentials: 'include'` cookie is sent
- Confirm the token format (classic `ghp_` or fine-grained `github_pat_...`) and that the selected `kind` is accepted by the server
- Inspect server logs for /web/github/pat if the status is 5xx
Example fix
// before
catch (e) { console.error('save failed'); }
// after
try {
await saveGithubPat(baseUrl, token, kind);
} catch (e) {
if (/401|403/.test(String(e))) {
await refreshSession(); // re-authenticate, then retry
}
showToast(e.message); // surface server error text to the user
} Defensive patterns
Strategy: try-catch
Validate before calling
function canAttemptPatSave(token: unknown, kind: string): boolean {
return typeof token === 'string' && token.trim().length > 0 && /^gh[pousr]_|^github_pat_/.test(token.trim()) && typeof kind === 'string' && kind.length > 0;
} Type guard
function isPatErrorBody(b: unknown): b is { error: string } {
return typeof b === 'object' && b !== null && typeof (b as { error?: unknown }).error === 'string';
} Try / catch
try {
await saveGithubPat(baseUrl, token, kind);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (/\((401|403)\)/.test(msg)) promptReauth();
else showFormError(isServerErrorPayload(msg) ? msg : 'Could not save GitHub token');
} Prevention
- Validate the PAT format client-side before submitting
- Check session validity (ping an authed endpoint) before showing the token form
- Surface the server-provided error text to the user instead of a generic toast
- Keep baseUrl on the same origin as the session cookie so credentials: 'include' works
- Never retry the save automatically on 400 — invalid tokens won't fix themselves
When it happens
Trigger: Any non-ok response from POST /web/github/pat: 401 when the session cookie is missing/expired, 403 when the user lacks workspace permissions, 400 when the token is malformed or kind is invalid, 500 on server-side validation or GitHub API failure while verifying the PAT.
Common situations: User pastes an expired/revoked GitHub PAT; session cookie not sent because baseUrl points to a different origin without CORS credentials; server rejects the token during GitHub verification; auth session timed out mid-form.
Related errors
- Failed to remove GitHub token (${res.status})
- Failed to list repos (${res.status})
- GitHub OAuth token exchange failed: ${res.status}
- GitHub capabilities require a GitHub connection.
- Repository access did not include a bearer token for the Fac
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/aebbf42e4f56b67e.
Report an issue: GitHub.