toeverything/AFFiNE · error · TooManyRequest
too_many_request
too_many_request
Error message
Too many requests.
What it means
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.
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.
Example fix
// before
await inviteByEmail(workspaceId, allEmails); // 2000 emails -> TooManyRequest
// after
const CHUNK = 512;
const results = [];
for (let i = 0; i < allEmails.length; i += CHUNK) {
results.push(...await inviteByEmail(workspaceId, allEmails.slice(i, i + CHUNK)));
} Defensive patterns
Strategy: validation
Validate before calling
const MAX_INVITE_BATCH = 512;
function chunk<T>(arr: T[], size: number): T[][] {
const out: T[][] = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
const batches = chunk(emails, MAX_INVITE_BATCH); // never send more than 512 in one call Type guard
function isTooManyRequestError(e: unknown): boolean {
const ext = (e as { extensions?: Record<string, unknown> })?.extensions;
return String(ext?.name ?? '').toLowerCase() === 'too_many_request';
} Try / catch
try {
return await inviteByEmail(workspaceId, emails);
} catch (e) {
if (isTooManyRequestError(e) && emails.length > 512) {
const out: InviteResult[] = [];
for (const b of chunk(emails, 512)) out.push(...await inviteByEmail(workspaceId, b));
return out;
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Related errors
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/fbaff62abfe3be39.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/workspaces/resolvers/member.ts:186
}));
}
}
@Mutation(() => [InviteResult])
async inviteMembers(
@CurrentUser() me: CurrentUser,
@Context() context: GraphqlContext,
@Args('workspaceId') workspaceId: string,
@Args({ name: 'emails', type: () => [String] }) emails: string[]
): Promise<InviteResult[]> {
await this.ac
.user(me.id)
.workspace(workspaceId)
.assert('Workspace.Users.Manage');
await this.assertWorkspaceNameCanInvite(workspaceId);
if (emails.length > 512) {
throw new TooManyRequest();
}
const results: InviteResult[] = emails.map(email => ({ email }));
const candidates: InviteCandidate[] = [];
const seen = new Set<string>();
for (const [index, email] of emails.entries()) {
try {
const normalizedEmail = email.trim().toLowerCase();
validators.assertValidEmail(normalizedEmail);
if (seen.has(normalizedEmail)) {
throw new ActionForbidden('Duplicate invite email.');
}
seen.add(normalizedEmail);
const target = await this.models.user.getUserByEmail(normalizedEmail);
if (target) {
const originRecord = await this.models.workspaceUser.get(
workspaceId,View on GitHub (pinned to 2af30773ae)