toeverything/AFFiNE · warning · InvalidAppConfigInput

invalid_app_config_input

invalid_app_config_input

Error message

Invalid app config input: ${message}

What it means

Thrown by `ServerService.updateConfig` when `configFactory.validate(updates)` returns one or more validation errors. The error messages are joined with newlines and passed as `message` on `InvalidAppConfigInput` (category `invalid_input`), aborting the save before anything reaches the `appConfig` table.

Source

Thrown at packages/backend/server/src/core/config/service.ts:79

    this.#features.delete(feature);
  }

  getConfig() {
    return this.configFactory.clone();
  }

  validateConfig(updates: Array<{ module: string; key: string; value: any }>) {
    return this.configFactory.validate(updates);
  }

  async updateConfig(
    user: string,
    updates: Array<{ module: string; key: string; value: any }>
  ): Promise<DeepPartial<AppConfig>> {
    const errors = this.validateConfig(updates);

    if (errors?.length) {
      throw new InvalidAppConfigInput({
        message: errors.map(error => error.message).join('\n'),
      });
    }

    const promises = await this.models.appConfig.save(
      user,
      updates.map(update => ({
        key: `${update.module}.${update.key}`,
        value: update.value,
      }))
    );

    const overrides: DeepPartial<AppConfig> = {};
    // only take successfully saved configs
    promises.forEach(promise => {
      if (promise.status === 'fulfilled') {
        set(overrides, promise.value.id, promise.value.value);
      } else {

View on GitHub (pinned to 26c515e050)

Solutions

  1. Run `server.validateConfig(updates)` (or the same schema on the client) before calling `updateConfig` and show per-field errors.
  2. Inspect the returned `message` string — it lists every failing field; fix each cited `module.key`.
  3. Re-fetch the config descriptor/schema after a server upgrade to learn the current valid keys and types.
  4. Ensure each entry uses `{ module, key, value }` shape and that `value` matches the declared type.

Example fix

// before
await server.updateConfig(userId, updates);

// after
const errors = server.validateConfig(updates);
if (errors?.length) {
  // errors: [{ field, message }...] — surface per-field in the admin form
  setFieldErrors(errors);
  return;
}
await server.updateConfig(userId, updates);
Defensive patterns

Strategy: validation

Validate before calling

// Validate config updates against the same factory before submitting
const updates = [
  { module: 'auth', key: 'session.timeout', value: 3600 },
];

const errors = server.validateConfig(updates);
if (errors?.length) {
  // errors: [{ field, message }, ...]
  setFieldErrors(errors.map(e => ({ path: e.field, message: e.message })));
  return;
}

await server.updateConfig(userId, updates);

Type guard

interface ConfigUpdate {
  module: string;
  key: string;
  value: unknown;
}
function isConfigUpdate(v: unknown): v is ConfigUpdate {
  return typeof v === 'object' && v !== null &&
    typeof (v as ConfigUpdate).module === 'string' &&
    typeof (v as ConfigUpdate).key === 'string' &&
    'value' in v;
}

Try / catch

try {
  await server.updateConfig(userId, updates);
} catch (e) {
  if (e?.extensions?.code === 'invalid_app_config_input') {
    // e.extensions.message lists every failing field, newline-separated
    setFormErrors(e.extensions.message.split('\n'));
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Submitting a config update whose `module.key` path is unknown, whose value type mismatches the schema, or whose value violates a declared constraint (range/enum/format).

Common situations: Admin UI posting a mistyped key or wrong-type value; version drift where a config key was renamed/removed but the caller still sends the old shape; copy-pasted JSON with the wrong nesting.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/112cadb859e13a14. Report an issue: GitHub.