{"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":"validation","errorClass":"BadRequest","httpStatus":400,"severity":"error","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/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/features/resolver.ts#L50-L86","documentation":"BadRequest thrown by the updateUserFeatures GraphQL mutation when one or more requested Feature values are outside the configurable set. configurableUserFeatures() is the allowlist of features an admin may toggle; anything else (e.g. internal or deprecated feature flags) is rejected. Correctly typed as BadRequest.","triggerScenarios":"Admin mutation sendFeatures called with a Feature enum value that isn't in configurableUserFeatures(). Enum is open (Feature type accepts the value) but the resolver narrows further at runtime.","commonSituations":"A feature was removed from the configurable list but the admin UI still offers it. A new enum variant added without being added to the configurable set. Client/server version skew where the client knows a feature the server no longer allows.","solutions":["Drive the admin UI's feature checkboxes from a server query of configurableUserFeatures() so only allowed features are selectable.","If a feature should be admin-configurable, add it to configurableUserFeatures() with intent.","On version skew, have the client filter its feature list against a server-provided allowlist before submitting.","Include the unsupported list (already done) so the admin can see exactly which entries to drop."],"exampleFix":"// before\nconst unsupported = features.filter(f => !configurableUserFeatures.has(f));\nif (unsupported.length) {\n  throw new BadRequest(`User feature ${unsupported.join(', ')} is not configurable`);\n}\n\n// after — also strip client-side to avoid round-tripping\nconst safe = features.filter(f => configurableUserFeatures.has(f));\nif (safe.length !== features.length) {\n  throw new BadRequest(`User feature ${features.filter(f => !configurableUserFeatures.has(f)).join(', ')} is not configurable`);\n}","handlingStrategy":"validation","validationCode":"// Fetch the configurable set from the server and intersect before submit\nconst configurable = await client.query({ query: CONFIGURABLE_USER_FEATURES });\nconst safe = requestedFeatures.filter(f => configurable.includes(f));\nif (safe.length !== requestedFeatures.length) {\n  throw new UserError('Some features are not admin-configurable');\n}\nawait client.mutate({ mutation: UPDATE_USER_FEATURES, variables: { id, features: safe } });","typeGuard":"import { BadRequest } from '<app errors>';\nfunction isFeatureNotConfigurable(e: unknown): boolean {\n  return e instanceof BadRequest && /not configurable/.test(e.message);\n}","tryCatchPattern":"try {\n  await resolver.updateUserFeatures(id, features);\n} catch (e) {\n  if (isFeatureNotConfigurable(e)) {\n    return res.status(400).send(e.message); // includes the unsupported list\n  }\n  throw e;\n}","preventionTips":["Source feature checkboxes from the server's configurable list.","Handle version skew by re-fetching the allowlist on error and retrying with the filtered set.","Add new features to configurableUserFeatures when they should be admin-toggleable."],"tags":["features","admin","validation","input-validation","graphql"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}