{"record":{"id":"939a638bb254005d","repo":"mastra-ai/mastra","slug":"failed-to-unlink-repository-res-status","errorCode":null,"errorMessage":"Failed to unlink repository (${res.status})","messagePattern":"Failed to unlink repository \\((.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory-ui/src/ui/domains/workspaces/services/github.ts","lineNumber":444,"sourceCode":"    'Failed to link GitHub repository',\n  );\n  return toLinkedRepositoryPayload({ id: factoryProjectId, name: repo.fullName }, projectRepository);\n}\n\n/**\n * Unlink a repository from its Factory project. Missing links are treated as\n * already removed so unlink stays idempotent.\n */\nexport async function unlinkRepository(\n  baseUrl: string,\n  factoryProjectId: string,\n  projectRepositoryId: string,\n): Promise<void> {\n  const res = await fetch(\n    `${baseUrl}/web/factory/projects/${encodeURIComponent(factoryProjectId)}/repositories/${encodeURIComponent(projectRepositoryId)}`,\n    { method: 'DELETE', credentials: 'include', headers: { Accept: 'application/json' } },\n  );\n  if (!res.ok && res.status !== 404) throw new Error(`Failed to unlink repository (${res.status})`);\n}\n\n/**\n * Delete a Factory project. The server cascades over its source-control\n * connections (and their repository links). Missing projects are treated as\n * already deleted so removal stays idempotent.\n */\nexport async function deleteFactoryProject(baseUrl: string, factoryProjectId: string): Promise<void> {\n  const res = await fetch(`${baseUrl}/web/factory/projects/${encodeURIComponent(factoryProjectId)}`, {\n    method: 'DELETE',\n    credentials: 'include',\n    headers: { Accept: 'application/json' },\n  });\n  if (!res.ok && res.status !== 404) throw new Error(`Failed to delete Factory (${res.status})`);\n}\n\nexport interface CommitResult {\n  committed: boolean;","sourceCodeStart":426,"sourceCodeEnd":462,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory-ui/src/ui/domains/workspaces/services/github.ts#L426-L462","documentation":"unlinkRepository sends DELETE to `/web/factory/projects/{id}/repositories/{repoId}` and throws this error for any non-ok status except 404, which is deliberately tolerated so unlinking stays idempotent. It means an existing repository link could not be removed.","triggerScenarios":"Non-ok, non-404 response: 401 unauthenticated session, 403 user lacks access to the Factory project, 400/422 malformed factoryProjectId or projectRepositoryId, 409 the link is referenced/locked, 5xx server failure during cascade cleanup.","commonSituations":"Unlinking while another user already deleted the project (404 — correctly swallowed); permissions changed so the current user is no longer a project member; expired session; passing a repo id from a different project.","solutions":["Check the status in the message: 401/403 → re-authenticate or confirm project membership","Ensure projectRepositoryId belongs to the given factoryProjectId and both ids are the server-side ids (not display names)","Treat as best-effort: catch and refresh the project's repository list to see the actual state","For 5xx, retry after a short delay; cascade cleanup may be retried safely","Do not change code to also swallow 409/400 — those indicate a real mismatch that should surface"],"exampleFix":"// before\nawait unlinkRepository(baseUrl, projectId, repoId);\n// after\ntry {\n  await unlinkRepository(baseUrl, projectId, repoId);\n} catch (e) {\n  if (/\\(40[13]\\)/.test(e.message)) {\n    await reauth();\n    return unlinkRepository(baseUrl, projectId, repoId);\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"function canAttemptUnlink(ids: { factoryProjectId?: unknown; projectRepositoryId?: unknown }): boolean {\n  return typeof ids.factoryProjectId === 'string' && ids.factoryProjectId.length > 0 && typeof ids.projectRepositoryId === 'string' && ids.projectRepositoryId.length > 0;\n}","typeGuard":"const isBenignDeleteError = (e: unknown): boolean => e instanceof Error && /\\(404\\)/.test(e.message);","tryCatchPattern":"try {\n  await unlinkRepository(baseUrl, factoryProjectId, projectRepositoryId);\n} catch (e) {\n  if (isBenignDeleteError(e)) {\n    // already unlinked: refresh and continue\n  } else if (/\\((401|403)\\)/.test(e.message)) {\n    promptReauth();\n  } else {\n    toast.error(e.message);\n  }\n} finally {\n  queryClient.invalidateQueries(projectRepositoriesKey(factoryProjectId));\n}","preventionTips":["Always invalidate the repository list after unlink, success or tolerated-404","Match repo ids to the correct project before calling","Check membership/permission on the project before showing the unlink action","Keep the 404-tolerance contract: never surface 404 to users as an error","Debounce unlink actions to prevent racing duplicate DELETEs"],"tags":["http","fetch","factory","idempotency"],"backgroundTag":"http-delete-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}