toeverything/AFFiNE · error · BadRequest
bad_request
bad_request
Error message
User feature ${unsupported.join(', ')} is not configurable What it means
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.
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 26c515e050)
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.
Example fix
// before
const unsupported = features.filter(f => !configurableUserFeatures.has(f));
if (unsupported.length) {
throw new BadRequest(`User feature ${unsupported.join(', ')} is not configurable`);
}
// after — also strip client-side to avoid round-tripping
const safe = features.filter(f => configurableUserFeatures.has(f));
if (safe.length !== features.length) {
throw new BadRequest(`User feature ${features.filter(f => !configurableUserFeatures.has(f)).join(', ')} is not configurable`);
} Defensive patterns
Strategy: validation
Validate before calling
// Fetch the configurable set from the server and intersect before submit
const configurable = await client.query({ query: CONFIGURABLE_USER_FEATURES });
const safe = requestedFeatures.filter(f => configurable.includes(f));
if (safe.length !== requestedFeatures.length) {
throw new UserError('Some features are not admin-configurable');
}
await client.mutate({ mutation: UPDATE_USER_FEATURES, variables: { id, features: safe } }); Type guard
import { BadRequest } from '<app errors>';
function isFeatureNotConfigurable(e: unknown): boolean {
return e instanceof BadRequest && /not configurable/.test(e.message);
} Try / catch
try {
await resolver.updateUserFeatures(id, features);
} catch (e) {
if (isFeatureNotConfigurable(e)) {
return res.status(400).send(e.message); // includes the unsupported list
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/fd21f9f340128e89.
Report an issue: GitHub.