{"record":{"id":"112cadb859e13a14","repo":"toeverything/AFFiNE","slug":"invalid-app-config-input-112cad","errorCode":"invalid_app_config_input","errorMessage":"Invalid app config input: ${message}","messagePattern":"Invalid app config input: (.+?)","errorType":"validation","errorClass":"InvalidAppConfigInput","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/core/config/service.ts","lineNumber":79,"sourceCode":"    this.#features.delete(feature);\n  }\n\n  getConfig() {\n    return this.configFactory.clone();\n  }\n\n  validateConfig(updates: Array<{ module: string; key: string; value: any }>) {\n    return this.configFactory.validate(updates);\n  }\n\n  async updateConfig(\n    user: string,\n    updates: Array<{ module: string; key: string; value: any }>\n  ): Promise<DeepPartial<AppConfig>> {\n    const errors = this.validateConfig(updates);\n\n    if (errors?.length) {\n      throw new InvalidAppConfigInput({\n        message: errors.map(error => error.message).join('\\n'),\n      });\n    }\n\n    const promises = await this.models.appConfig.save(\n      user,\n      updates.map(update => ({\n        key: `${update.module}.${update.key}`,\n        value: update.value,\n      }))\n    );\n\n    const overrides: DeepPartial<AppConfig> = {};\n    // only take successfully saved configs\n    promises.forEach(promise => {\n      if (promise.status === 'fulfilled') {\n        set(overrides, promise.value.id, promise.value.value);\n      } else {","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/config/service.ts#L61-L97","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Run `server.validateConfig(updates)` (or the same schema on the client) before calling `updateConfig` and show per-field errors.","Inspect the returned `message` string — it lists every failing field; fix each cited `module.key`.","Re-fetch the config descriptor/schema after a server upgrade to learn the current valid keys and types.","Ensure each entry uses `{ module, key, value }` shape and that `value` matches the declared type."],"exampleFix":"// before\nawait server.updateConfig(userId, updates);\n\n// after\nconst errors = server.validateConfig(updates);\nif (errors?.length) {\n  // errors: [{ field, message }...] — surface per-field in the admin form\n  setFieldErrors(errors);\n  return;\n}\nawait server.updateConfig(userId, updates);","handlingStrategy":"validation","validationCode":"// Validate config updates against the same factory before submitting\nconst updates = [\n  { module: 'auth', key: 'session.timeout', value: 3600 },\n];\n\nconst errors = server.validateConfig(updates);\nif (errors?.length) {\n  // errors: [{ field, message }, ...]\n  setFieldErrors(errors.map(e => ({ path: e.field, message: e.message })));\n  return;\n}\n\nawait server.updateConfig(userId, updates);","typeGuard":"interface ConfigUpdate {\n  module: string;\n  key: string;\n  value: unknown;\n}\nfunction isConfigUpdate(v: unknown): v is ConfigUpdate {\n  return typeof v === 'object' && v !== null &&\n    typeof (v as ConfigUpdate).module === 'string' &&\n    typeof (v as ConfigUpdate).key === 'string' &&\n    'value' in v;\n}","tryCatchPattern":"try {\n  await server.updateConfig(userId, updates);\n} catch (e) {\n  if (e?.extensions?.code === 'invalid_app_config_input') {\n    // e.extensions.message lists every failing field, newline-separated\n    setFormErrors(e.extensions.message.split('\\n'));\n    return;\n  }\n  throw e;\n}","preventionTips":["Always call `validateConfig` before `updateConfig` and surface per-field errors.","After a server upgrade, re-fetch the config descriptor to learn valid keys/types.","Send each update as `{ module, key, value }` with a value matching the declared type.","Parse the returned `message` newline-list to fix every cited field at once."],"tags":["config","validation","admin","graphql","schema"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}