{"record":{"id":"fbaff62abfe3be39","repo":"toeverything/AFFiNE","slug":"too-many-request-fbaff6","errorCode":"too_many_request","errorMessage":"Too many requests.","messagePattern":"Too many requests\\.","errorType":"exception","errorClass":"TooManyRequest","httpStatus":429,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":186,"sourceCode":"      }));\n    }\n  }\n\n  @Mutation(() => [InviteResult])\n  async inviteMembers(\n    @CurrentUser() me: CurrentUser,\n    @Context() context: GraphqlContext,\n    @Args('workspaceId') workspaceId: string,\n    @Args({ name: 'emails', type: () => [String] }) emails: string[]\n  ): Promise<InviteResult[]> {\n    await this.ac\n      .user(me.id)\n      .workspace(workspaceId)\n      .assert('Workspace.Users.Manage');\n    await this.assertWorkspaceNameCanInvite(workspaceId);\n\n    if (emails.length > 512) {\n      throw new TooManyRequest();\n    }\n\n    const results: InviteResult[] = emails.map(email => ({ email }));\n    const candidates: InviteCandidate[] = [];\n    const seen = new Set<string>();\n    for (const [index, email] of emails.entries()) {\n      try {\n        const normalizedEmail = email.trim().toLowerCase();\n        validators.assertValidEmail(normalizedEmail);\n        if (seen.has(normalizedEmail)) {\n          throw new ActionForbidden('Duplicate invite email.');\n        }\n        seen.add(normalizedEmail);\n\n        const target = await this.models.user.getUserByEmail(normalizedEmail);\n        if (target) {\n          const originRecord = await this.models.workspaceUser.get(\n            workspaceId,","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/resolvers/member.ts#L168-L204","documentation":"Thrown by the inviteByEmail mutation when the emails array contains more than 512 entries. It caps a single invite batch to keep the per-email processing loop (user lookups/creation, membership checks, seat accounting) bounded. Code too_many_request, HTTP 429; unlike per-email failures, this aborts the whole mutation before any candidate is processed.","triggerScenarios":"Calling inviteByEmail with emails.length > 512 — e.g. pasting a large distribution list or syncing an org directory in one request. The check runs after permission and workspace-name checks, so nothing is invited when it fires.","commonSituations":"Bulk onboarding scripts that send the whole CSV at once; HR/IT teams importing a company-wide list; UI that lets users paste unlimited lines into a textarea bound to the invite mutation.","solutions":["Split the list into chunks of at most 512 emails and call inviteByEmail per chunk.","Preferably chunk smaller (e.g. 100) so per-email results are easier to review and rate limits are not tripped.","Add a client-side count guard: disable the invite button when emails.length > 512 with a clear message."],"exampleFix":"// before\nawait inviteByEmail(workspaceId, allEmails); // 2000 emails -> TooManyRequest\n\n// after\nconst CHUNK = 512;\nconst results = [];\nfor (let i = 0; i < allEmails.length; i += CHUNK) {\n  results.push(...await inviteByEmail(workspaceId, allEmails.slice(i, i + CHUNK)));\n}","handlingStrategy":"validation","validationCode":"const MAX_INVITE_BATCH = 512;\nfunction chunk<T>(arr: T[], size: number): T[][] {\n  const out: T[][] = [];\n  for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));\n  return out;\n}\nconst batches = chunk(emails, MAX_INVITE_BATCH); // never send more than 512 in one call","typeGuard":"function isTooManyRequestError(e: unknown): boolean {\n  const ext = (e as { extensions?: Record<string, unknown> })?.extensions;\n  return String(ext?.name ?? '').toLowerCase() === 'too_many_request';\n}","tryCatchPattern":"try {\n  return await inviteByEmail(workspaceId, emails);\n} catch (e) {\n  if (isTooManyRequestError(e) && emails.length > 512) {\n    const out: InviteResult[] = [];\n    for (const b of chunk(emails, 512)) out.push(...await inviteByEmail(workspaceId, b));\n    return out;\n  }\n  throw e;\n}","preventionTips":["Chunk large imports client-side (smaller chunks also make per-email results reviewable).","Show a count of pending invites before submit so users see the batch size."],"tags":["affine","graphql","invite","batch-limit","too-many-requests"],"backgroundTag":"batch-size-limit-exceeded","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}