{"record":{"id":"9b9544b86177c9cd","repo":"toeverything/AFFiNE","slug":"too-many-request-9b9544","errorCode":"too_many_request","errorMessage":"Server is busy","messagePattern":"Server is busy","errorType":"exception","errorClass":"TooManyRequest","httpStatus":429,"severity":"warning","filePath":"packages/backend/server/src/plugins/copilot/workspace/resolver.ts","lineNumber":175,"sourceCode":"    @CurrentUser() user: CurrentUser,\n    @Args('workspaceId', { type: () => String })\n    workspaceId: string,\n    @Args({ name: 'blob', type: () => GraphQLUpload })\n    content: FileUpload\n  ): Promise<CopilotWorkspaceArtifactType> {\n    await this.ac\n      .user(user.id)\n      .workspace(workspaceId)\n      .assert('Workspace.Settings.Update');\n\n    if (!this.copilotWorkspace.canEmbedding) {\n      throw new CopilotEmbeddingUnavailable();\n    }\n\n    const lockFlag = `${COPILOT_LOCKER}:workspace:${workspaceId}`;\n    await using lock = await this.mutex.acquire(lockFlag);\n    if (!lock) {\n      throw new TooManyRequest('Server is busy');\n    }\n\n    const length = Number(ctx.req.headers['content-length']);\n    if (length && length >= MAX_EMBEDDABLE_SIZE) {\n      throw new BlobQuotaExceeded();\n    }\n\n    try {\n      return await this.copilotWorkspace.addArtifact(workspaceId, content);\n    } catch (e) {\n      // passthrough user friendly error\n      if (e instanceof UserFriendlyError) {\n        throw e;\n      }\n      throw new CopilotFailedToAddWorkspaceArtifact({\n        message: e instanceof Error ? e.message : String(e),\n      });\n    }","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/workspace/resolver.ts#L157-L193","documentation":"addWorkspaceArtifact serializes work per workspace with a mutex keyed copilot-locker:workspace:<workspaceId>. If another in-flight request already holds the lock, mutex.acquire returns a falsy lock and the resolver throws TooManyRequest('Server is busy') (code too_many_request) instead of queueing.","triggerScenarios":"Two concurrent addWorkspaceArtifact mutations for the same workspace: double-clicked upload, multiple tabs uploading, or a client retry overlapping the original slow request (large file embedding takes a while).","commonSituations":"Aggressive client retry on timeout while the first upload is still processing; batch scripts pushing many artifacts into one workspace in parallel; upload button not disabled during flight.","solutions":["Serialize uploads per workspace on the client - finish one request before starting the next","Retry after a short delay; the lock is released when the in-flight upload completes","Disable the submit control while a mutation is pending to prevent double submits"],"exampleFix":"// before\nartifacts.forEach(a => mutate({ workspaceId, blob: a })); // parallel -> 429\n\n// after\nfor (const a of artifacts) {\n  await mutate({ workspaceId, blob: a }); // one at a time per workspace\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for (let attempt = 1; attempt <= 5; attempt++) {\n  try {\n    return await mutate({ workspaceId, blob });\n  } catch (e) {\n    if (e?.code !== 'too_many_request' || attempt === 5) throw e;\n    await sleep(2 ** attempt * 250); // wait for the in-flight upload to finish\n  }\n}","preventionTips":["Serialize artifact uploads per workspace client-side; one request at a time","Do not retry uploads immediately on timeout - the original may still hold the lock","Disable the upload control while a mutation is pending to avoid double submits"],"tags":["copilot","mutex","concurrency","rate-limit","upload"],"backgroundTag":"too-many-requests","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}