Files
ComfyUI_frontend/src/stores/widgetStore.ts
Matt Miller f110af79f7 fix(widgetStore): tolerate null/undefined custom widgets from extensions (#12728)
## ELI-5

Some custom nodes have a `getCustomWidgets()` function that's *supposed*
to hand
us a list of widgets. A few of them hand us back nothing
(null/undefined)
instead. We were trying to read that "nothing" like a list, which
crashes with
*"Cannot convert undefined or null to object"* — and because it happens
while
the app is still starting up, it can break the whole page. This PR just
says
"if there's nothing to register, skip it."

## What

`registerCustomWidgets` called `Object.entries(newWidgets)` directly.
When an
extension's `getCustomWidgets()` resolves to `null`/`undefined` (it's
typed
non-null, but extensions are untrusted and routinely violate the type),
this
throws `TypeError: Cannot convert undefined or null to object`.

The call site in `extensionService.ts` runs this inside a bare async
IIFE,
*outside* the `wrapWithErrorHandling` wrappers used for
keybindings/settings, so
the throw is unhandled and surfaces during app initialization.

## Why it matters

In production this is one of the highest-volume unhandled frontend
errors —
~2.6k events across **~1,160 distinct sessions/day**, all funneling
through this
one `Object.entries` call. Guarding the choke point silences it for
every
caller.

## Fix

- Keep `registerCustomWidgets` typed `Record<string,
ComfyWidgetConstructor>`
(the correct internal contract) and early-return on nullish input. The
runtime
guard defends against untrusted extensions that violate the type at the
  boundary, without weakening the signature for legitimate callers.
- Add a regression test asserting
`registerCustomWidgets(null!/undefined!)` does
  not throw (the `!` casts simulate the boundary violation).

## Test plan

- [x] `npx vitest run src/stores/widgetStore.test.ts` — 8 passing,
including the
  new null/undefined case.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-09 18:32:51 +00:00

41 lines
1.5 KiB
TypeScript

import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { getInputSpecType } from '@/schemas/nodeDefSchema'
import type { InputSpec as InputSpecV1 } from '@/schemas/nodeDefSchema'
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
import { ComfyWidgets } from '@/scripts/widgets'
export const useWidgetStore = defineStore('widget', () => {
const coreWidgets = ComfyWidgets
const customWidgets = ref<Map<string, ComfyWidgetConstructor>>(new Map())
const widgets = computed<Map<string, ComfyWidgetConstructor>>(
() => new Map([...customWidgets.value, ...Object.entries(coreWidgets)])
)
function inputIsWidget(spec: InputSpecV2 | InputSpecV1) {
const type = Array.isArray(spec) ? getInputSpecType(spec) : spec.type
return widgets.value.has(type)
}
function registerCustomWidgets(
newWidgets: Record<string, ComfyWidgetConstructor>
) {
// Extensions are untrusted code: `getCustomWidgets` is typed to return
// `Record<string, ...>`, but in practice an extension can resolve it to
// null/undefined. Guard here so a single misbehaving custom node can't
// throw "Cannot convert undefined or null to object" and break app init.
if (!newWidgets) return
for (const [type, widget] of Object.entries(newWidgets)) {
customWidgets.value.set(type, widget)
}
}
return {
widgets,
inputIsWidget,
registerCustomWidgets
}
})