{"record":{"id":"a1d2429f29db5932","repo":"Budibase/budibase","slug":"oauth2-config-with-name-name-is-already-taken","errorCode":null,"errorMessage":"OAuth2 config with name '${name}' is already taken.","messagePattern":"OAuth2 config with name '(.+?)' is already taken\\.","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/oauth2/crud.ts","lineNumber":23,"sourceCode":"  HTTPError,\n  utils,\n} from \"@budibase/backend-core\"\nimport {\n  DocumentType,\n  OAuth2Config,\n  PASSWORD_REPLACEMENT,\n  SEPARATOR,\n  WithoutDocMetadata,\n  WithRequired,\n} from \"@budibase/types\"\n\ntype CreatedOAuthConfig = WithRequired<OAuth2Config, \"_id\" | \"_rev\">\n\nasync function guardName(name: string, id?: string) {\n  const existingConfigs = await fetch()\n\n  if (existingConfigs.find(c => c.name === name && c._id !== id)) {\n    throw new HTTPError(\n      `OAuth2 config with name '${name}' is already taken.`,\n      400\n    )\n  }\n}\n\nexport async function fetch(): Promise<CreatedOAuthConfig[]> {\n  const db = context.getWorkspaceDB()\n  const docs = await db.allDocs<OAuth2Config>(\n    docIds.getOAuth2ConfigParams(null, { include_docs: true })\n  )\n  const result = docs.rows.map(r => ({\n    ...r.doc!,\n    _id: r.doc!._id!,\n    _rev: r.doc!._rev!,\n  }))\n  return result\n}","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/oauth2/crud.ts#L5-L41","documentation":"guardName enforces unique OAuth2 config names within a workspace. Before create (no id) or update (id to exclude), it fetches all configs and throws HTTP 400 if another config with the same name exists under a different _id. Names are user-facing identifiers used to select configs, so duplicates are rejected.","triggerScenarios":"Creating a new OAuth2 config whose name collides with an existing one, or renaming a config via update to a name already held by a different config — guardName excludes only the config being updated (matching _id).","commonSituations":"Two users creating configs with the same provider name concurrently; renaming a config to match a sibling; automation/scripts re-running creates without idempotency checks.","solutions":["Choose a unique name — fetch existing configs and pick an unused one","If updating, ensure the update targets the existing config's own _id so guardName excludes it from the collision check","If a duplicate was created concurrently, delete or rename one of the duplicates"],"exampleFix":"// before\nawait oauth2.create({ name: \"acme\", ... }) // name already exists -> 400\n// after\nconst existing = await oauth2.fetch()\nconst name = existing.some(c => c.name === \"acme\") ? \"acme-2\" : \"acme\"\nawait oauth2.create({ name, ... })","handlingStrategy":"validation","validationCode":"const configs = await oauth2.fetch()\nif (configs.some(c => c.name === desiredName && c._id !== updatingId)) {\n  throw new Error(`Name '${desiredName}' is already taken — choose another`)","typeGuard":null,"tryCatchPattern":"try {\n  await oauth2.create(config)\n} catch (err) {\n  if (err instanceof HTTPError && err.status === 400 && /already taken/.test(err.message)) {\n    const uniqueName = `${config.name}-${Date.now()}`\n    return oauth2.create({ ...config, name: uniqueName })\n  }\n  throw err\n}","preventionTips":["List existing OAuth2 configs before choosing a name","For updates, always pass the config's own _id so its current name is excluded from the check","Make automation create-steps idempotent: search by name first, update if found"],"tags":["oauth2","duplicate-name","uniqueness","http-400"],"backgroundTag":"duplicate-resource-name","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}