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

  1. 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
  2. Check the numeric status in the fallback message; 401/403 means re-authenticate or verify workspace permissions before retrying
  3. Verify the user is logged in and baseUrl targets the correct server origin so the `credentials: 'include'` cookie is sent
  4. Confirm the token format (classic `ghp_` or fine-grained `github_pat_...`) and that the selected `kind` is accepted by the server
  5. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/aebbf42e4f56b67e. Report an issue: GitHub.