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

  1. Drive the admin UI's feature checkboxes from a server query of configurableUserFeatures() so only allowed features are selectable.
  2. If a feature should be admin-configurable, add it to configurableUserFeatures() with intent.
  3. On version skew, have the client filter its feature list against a server-provided allowlist before submitting.
  4. 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

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.