Repository navigation
Replies: 1 comment
|
The failure comes from the null check in "SettingsForms.schema()": return schema !== undefined && 'toJSON' in schema The condition excludes "undefined" but still allows "null", so the "in" operator throws when a plugin exports "Config = null". The minimal fix is to change "schema !== undefined" to "schema != null": return schema != null && 'toJSON' in schema This excludes both "null" and "undefined", allowing the schema traversal to skip plugins without a usable schema instead of aborting the entire "describe()" call. A regression test should cover all three cases: a valid schema, "undefined", and "null". The discussion author reports that the one-line fix restored the failing settings endpoints in a live installation. Source location and reproduction: #7900 |
Uh oh!
There was an error while loading. Please reload this page.
TL;DR
SettingsForms.schema()checksschema !== undefinedbut notnull. A plugin that exportsConfig = null— a legitimate JS-level fallback when an optional schema build fails — makesdescribe()throwTypeError: Cannot use 'in' operator to search for 'toJSON' in null. The throw aborts the entiredescribe()walk, so every consumer that callsctx.settings.describe()during activation gets its fiber marked errored, and all subsequent settings reads/writes fail with:The reported error points at Cordis lifecycle semantics, while the actual cause is a null-safety gap one function away. A one-line fix resolves it (verified on a live 0.1.7-rc.2 install).
Affected code
packages/settings/settings/src/index.ts(master, last touched 2026-09-21 in #4587):runtime.Configis typedz | undefined, so the type system saysnullcan't occur. ButConfigis a plain JS module export — a plugin can legally producenullthere. We hit this in the wild (see below), so the type assumption does not hold in practice.Failure cascade
Config = nulland loads fine (its entry is ACTIVE).ctx.settings.describe()— in our casedsh-better-sidebar@0.21.1, whoseinject(['settings'])callback callsdescribe()synchronously — receives theTypeErrorinstead of a descriptor list.POST /sidebar/api/settings.get|update) fails:500/400withcannot get required service "settings" in inactive context.TypeErroralso fires fordsh-api-settings-controller'sdescribe()and the invalidation microtask insideSettingsForms.invalidate()— so the core settings UI is affected by the same walk, not just one plugin.Notably this is deterministic and survives restarts: it is not a race, a peer-range mismatch, or a lifecycle timing issue — every boot with the offending plugin present reproduces it.
Why this was hard to diagnose (secondary issue)
The thrown
TypeErroris invisible to operators. The plugin callback's failure is caught by Cordis'_reloadand routed toctx.logger.error(...), but dsh's logger does not surface plugin-callback errors on stderr by default. The only externally visible artifact is the downstream"inactive context"message, which sends investigation toward Cordis activation timing. It took a bisect over 31 installed plugins plus instrumenting the victim plugin's callback to see the real throw. If plugin-callback errors were printed once (plugin name + error), this class of issue would be a five-minute diagnosis.Minimal reproduction
Removing the offending plugin (or patching the one line below) immediately restores
200.Real-world trigger
@weibaohui/dsh-smart-title@0.1.5builds its SchemasteryConfigin a try/catch and intentionally degrades toConfig = nullwhen the schema library can't be resolved. On Windows its host-probe path pattern (<prefix>/lib/node_modules/...) never matches the npm-global layout (node_global\node_modules\...), so the fallback fires for every Windows user. That plugin's authors chose a reasonable JS-level fallback; the crash is on the consumer side. (Filed separately with the plugin.)Suggested fix
private schema(entry: Entry): z | undefined { const schema = entry.fiber?.runtime?.Config - return schema !== undefined && 'toJSON' in schema ? schema as z : undefined + return schema != null && 'toJSON' in schema ? schema as z : undefined }!= nullcovers bothundefinedandnull, matching the intent ("skip entries with no usable schema") without changing any other behavior.Verified: applied to the installed
@deepseek-ai/dsh-settings@0.1.7-rc.2on a live profile, the previously failingsettings.get/settings.updatecalls return200, the fulldescribe()walk completes, and all 31 installed plugins (including the null-Config one) keep working.Optional hardening, if you want the type system to admit reality instead of fighting it: type
runtime.Configasz | null | undefined, or treat any non-objectConfigas "no schema".Environment
0.1.7-rc.2(npmnext), Windows 11, Node 22dsh-better-sidebar@0.21.1,@weibaohui/dsh-smart-title@0.1.50.1.7-rc.2;mastersource still shows the unguarded check as of 2026-09-25All reactions