toeverything/AFFiNE · warning · BadRequest
bad_request
bad_request
Error message
User feature ${unsupported.join(', ')} is not configurable What it means
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.
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
Example fix
// before
await updateUserFeatures(userId, ['earlyAccess', 'admin']);
// after
const CONFIGURABLE = new Set([Feature.Admin]); // mirror of backend configurableUserFeatures()
const features = requested.filter(f => CONFIGURABLE.has(f));
if (requested.length !== features.length) throw new Error('non-configurable feature requested');
await updateUserFeatures(userId, features); Defensive patterns
Strategy: validation
Validate before calling
const CONFIGURABLE = new Set<UserFeatureName>(['Admin']); // mirror configurableUserFeatures()
const invalid = features.filter(f => !CONFIGURABLE.has(f));
if (invalid.length) throw new Error(`not configurable: ${invalid.join(', ')}`);
await updateUserFeatures(userId, features); Type guard
function isFeatureNotConfigurable(e: unknown): boolean {
const err = e as { extensions?: { code?: string }; message?: string };
return err.extensions?.code === 'bad_request' && /not configurable/.test(err.message ?? '');
} Try / catch
try {
await updateUserFeatures(userId, features);
} catch (e) {
if (isFeatureNotConfigurable(e)) {
return adminToast('Only Admin is admin-configurable in this version');
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- bad_request
- cannot_delete_own_account
- query_too_long
- workspace_id_required_to_update_team_subscription
- -32001
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/fd21f9f340128e89.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/features/resolver.ts:68
private readonly event: EventBus
) {
super();
}
@Mutation(() => [Feature], {
description: 'update user enabled feature',
})
async updateUserFeatures(
@Args('id') id: string,
@Args({ name: 'features', type: () => [Feature] })
features: UserFeatureName[]
) {
const configurableUserFeatures = this.configurableUserFeatures();
const unsupported = features.filter(
feature => !configurableUserFeatures.has(feature)
);
if (unsupported.length) {
throw new BadRequest(
`User feature ${unsupported.join(', ')} is not configurable`
);
}
const removed = difference(Array.from(configurableUserFeatures), features);
await Promise.all(
features.map(feature =>
this.models.userFeature.add(id, feature, 'admin panel')
)
);
await Promise.all(
removed.map(feature => this.models.userFeature.remove(id, feature))
);
const user = await this.models.user.get(id);
if (user) {
this.event.emit('user.updated', user);View on GitHub (pinned to b4c8548c09)