{"record":{"id":"aebbf42e4f56b67e","repo":"mastra-ai/mastra","slug":"body-error-or-failed-to-save-github-token-r","errorCode":null,"errorMessage":"${body?.error} or Failed to save GitHub token (${res.status})","messagePattern":"(.+?) or Failed to save GitHub token \\((.+?)\\)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory-ui/src/ui/domains/workspaces/services/github.ts","lineNumber":172,"sourceCode":"  const res = await fetch(`${baseUrl}/web/github/pat`, {\n    headers: { Accept: 'application/json' },\n    credentials: 'include',\n  });\n  if (!res.ok) throw new Error(`Failed to load GitHub token status (${res.status})`);\n  return (await res.json()) as GithubPatStatus;\n}\n\n/** Save an org GitHub PAT (used only for `gh` CLI auth in sandboxes). */\nexport async function saveGithubPat(baseUrl: string, token: string, kind: GithubPatKind = 'default'): Promise<void> {\n  const res = await fetch(`${baseUrl}/web/github/pat`, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n    credentials: 'include',\n    body: JSON.stringify({ token, kind }),\n  });\n  if (!res.ok) {\n    const body = (await res.json().catch(() => undefined)) as { error?: string } | undefined;\n    throw new Error(body?.error ?? `Failed to save GitHub token (${res.status})`);\n  }\n}\n\n/** Remove an org GitHub PAT. */\nexport async function deleteGithubPat(baseUrl: string, kind: GithubPatKind = 'default'): Promise<void> {\n  const res = await fetch(`${baseUrl}/web/github/pat?kind=${kind}`, {\n    method: 'DELETE',\n    headers: { Accept: 'application/json' },\n    credentials: 'include',\n  });\n  if (!res.ok) throw new Error(`Failed to remove GitHub token (${res.status})`);\n}\n\n/** List repos across the user's installations, optionally filtered by query. */\nexport async function listGithubRepos(baseUrl: string, query?: string): Promise<GithubRepo[]> {\n  const url = query ? `${baseUrl}/web/github/repos?q=${encodeURIComponent(query)}` : `${baseUrl}/web/github/repos`;\n  const res = await fetch(url, { headers: { Accept: 'application/json' }, credentials: 'include' });\n  if (!res.ok) throw new Error(`Failed to list repos (${res.status})`);","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory-ui/src/ui/domains/workspaces/services/github.ts#L154-L190","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ncatch (e) { console.error('save failed'); }\n// after\ntry {\n  await saveGithubPat(baseUrl, token, kind);\n} catch (e) {\n  if (/401|403/.test(String(e))) {\n    await refreshSession(); // re-authenticate, then retry\n  }\n  showToast(e.message); // surface server error text to the user\n}","handlingStrategy":"try-catch","validationCode":"function canAttemptPatSave(token: unknown, kind: string): boolean {\n  return typeof token === 'string' && token.trim().length > 0 && /^gh[pousr]_|^github_pat_/.test(token.trim()) && typeof kind === 'string' && kind.length > 0;\n}","typeGuard":"function isPatErrorBody(b: unknown): b is { error: string } {\n  return typeof b === 'object' && b !== null && typeof (b as { error?: unknown }).error === 'string';\n}","tryCatchPattern":"try {\n  await saveGithubPat(baseUrl, token, kind);\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  if (/\\((401|403)\\)/.test(msg)) promptReauth();\n  else showFormError(isServerErrorPayload(msg) ? msg : 'Could not save GitHub token');\n}","preventionTips":["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"],"tags":["http","github","auth","fetch"],"backgroundTag":"github-api-request-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}