{"record":{"id":"fd21f9f340128e89","repo":"toeverything/AFFiNE","slug":"bad-request-fd21f9","errorCode":"bad_request","errorMessage":"User feature ${unsupported.join(', ')} is not configurable","messagePattern":"User feature (.+?) is not configurable","errorType":"exception","errorClass":"BadRequest","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/core/features/resolver.ts","lineNumber":68,"sourceCode":"    private readonly event: EventBus\n  ) {\n    super();\n  }\n\n  @Mutation(() => [Feature], {\n    description: 'update user enabled feature',\n  })\n  async updateUserFeatures(\n    @Args('id') id: string,\n    @Args({ name: 'features', type: () => [Feature] })\n    features: UserFeatureName[]\n  ) {\n    const configurableUserFeatures = this.configurableUserFeatures();\n    const unsupported = features.filter(\n      feature => !configurableUserFeatures.has(feature)\n    );\n    if (unsupported.length) {\n      throw new BadRequest(\n        `User feature ${unsupported.join(', ')} is not configurable`\n      );\n    }\n    const removed = difference(Array.from(configurableUserFeatures), features);\n\n    await Promise.all(\n      features.map(feature =>\n        this.models.userFeature.add(id, feature, 'admin panel')\n      )\n    );\n\n    await Promise.all(\n      removed.map(feature => this.models.userFeature.remove(id, feature))\n    );\n\n    const user = await this.models.user.get(id);\n    if (user) {\n      this.event.emit('user.updated', user);","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/features/resolver.ts#L50-L86","documentation":"The updateUserFeatures admin mutation only accepts features present in AvailableUserFeatureConfig.configurableUserFeatures(), which currently contains exactly {Feature.Admin}. Any requested feature outside that set makes the resolver throw BadRequest listing the unsupported names. Features in the list but absent from the request are removed (difference + delete), so this is the gate for the whole add/remove flow.","triggerScenarios":"Sending features like 'earlyAccess' or 'copilot' via updateUserFeatures; an older admin UI sending feature names that are no longer admin-configurable; passing the full available-features list instead of the configurable subset.","commonSituations":"Frontend/backend version skew after the configurable set was narrowed; operators assuming every visible feature flag can be toggled per-user; automated scripts enumerating all Feature enum values.","solutions":["Restrict the request to the configurable set — today that is only Feature.Admin","Expose/consult configurableUserFeatures (or hardcode per backend version) in the admin UI so the picker only offers valid values","Re-sync the admin frontend with the backend release before granting features","On error, read the unsupported list from the message to see exactly which names were rejected"],"exampleFix":"// before\nawait updateUserFeatures(userId, ['earlyAccess', 'admin']);\n\n// after\nconst CONFIGURABLE = new Set([Feature.Admin]); // mirror of backend configurableUserFeatures()\nconst features = requested.filter(f => CONFIGURABLE.has(f));\nif (requested.length !== features.length) throw new Error('non-configurable feature requested');\nawait updateUserFeatures(userId, features);","handlingStrategy":"validation","validationCode":"const CONFIGURABLE = new Set<UserFeatureName>(['Admin']); // mirror configurableUserFeatures()\nconst invalid = features.filter(f => !CONFIGURABLE.has(f));\nif (invalid.length) throw new Error(`not configurable: ${invalid.join(', ')}`);\nawait updateUserFeatures(userId, features);","typeGuard":"function isFeatureNotConfigurable(e: unknown): boolean {\n  const err = e as { extensions?: { code?: string }; message?: string };\n  return err.extensions?.code === 'bad_request' && /not configurable/.test(err.message ?? '');\n}","tryCatchPattern":"try {\n  await updateUserFeatures(userId, features);\n} catch (e) {\n  if (isFeatureNotConfigurable(e)) {\n    return adminToast('Only Admin is admin-configurable in this version');\n  }\n  throw e;\n}","preventionTips":["Derive the admin UI's feature checkboxes from the backend's configurable set per version","Never send the full Feature enum; send only intended configurable toggles","Add contract tests that fail when the configurable set changes without a UI update"],"tags":["features","admin","graphql","validation"],"backgroundTag":"invalid-enum-value","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}