mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 14:54:37 +00:00
fix: show Missing Nodes dialog for missing node prompt errors (#8511)
## Summary When a workflow is queued with a missing node type, show the Missing Nodes dialog instead of a generic error toast. This gives users actionable options like "Open Manager" and "Install All". ## Changes - Detect `missing_node_type` error from backend in `queuePrompt()` catch block - Construct `MissingNodeType` object with class type and contextual hint - Reuse existing `showMissingNodesError()` to trigger the dialog ## Dependencies ⚠️ **Requires backend PR:** https://github.com/Comfy-Org/ComfyUI/pull/12177 The backend PR changes the error type from `invalid_prompt` to `missing_node_type` and adds `extra_info` with node details. ## Related - Fixes COM-12528 - Addresses ~49 GitHub issues with confusing "missing class_type" errors ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8511-fix-show-Missing-Nodes-dialog-for-missing-node-prompt-errors-2f96d73d3650812b95f0f08e51abaabb) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -70,6 +70,8 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
|
||||
import { type ExtensionManager } from '@/types/extensionTypes'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { graphToPrompt } from '@/utils/executionUtil'
|
||||
import type { MissingNodeTypeExtraInfo } from '@/workbench/extensions/manager/types/missingNodeErrorTypes'
|
||||
import { createMissingNodeTypeFromError } from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
|
||||
import { anyItemOverlapsRect } from '@/utils/mathUtil'
|
||||
import { collectAllNodes, forEachNode } from '@/utils/graphTraversalUtil'
|
||||
import {
|
||||
@@ -1394,10 +1396,21 @@ export class ComfyApp {
|
||||
} catch (error) {}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
useDialogService().showErrorDialog(error, {
|
||||
title: t('errorDialog.promptExecutionError'),
|
||||
reportType: 'promptExecutionError'
|
||||
})
|
||||
if (
|
||||
error instanceof PromptExecutionError &&
|
||||
typeof error.response.error === 'object' &&
|
||||
error.response.error?.type === 'missing_node_type'
|
||||
) {
|
||||
const extraInfo = (error.response.error.extra_info ??
|
||||
{}) as MissingNodeTypeExtraInfo
|
||||
const missingNodeType = createMissingNodeTypeFromError(extraInfo)
|
||||
this.showMissingNodesError([missingNodeType])
|
||||
} else {
|
||||
useDialogService().showErrorDialog(error, {
|
||||
title: t('errorDialog.promptExecutionError'),
|
||||
reportType: 'promptExecutionError'
|
||||
})
|
||||
}
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof PromptExecutionError) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Extra info returned by the backend for missing_node_type errors
|
||||
* from the /prompt endpoint validation.
|
||||
*/
|
||||
export interface MissingNodeTypeExtraInfo {
|
||||
class_type?: string | null
|
||||
node_title?: string | null
|
||||
node_id?: string
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
buildMissingNodeHint,
|
||||
createMissingNodeTypeFromError
|
||||
} from './missingNodeErrorUtil'
|
||||
|
||||
describe('buildMissingNodeHint', () => {
|
||||
it('returns hint with title and node ID when both available', () => {
|
||||
expect(buildMissingNodeHint('My Node', 'MyNodeClass', '42')).toBe(
|
||||
'"My Node" (Node ID #42)'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns hint with title only when no node ID', () => {
|
||||
expect(buildMissingNodeHint('My Node', 'MyNodeClass', undefined)).toBe(
|
||||
'"My Node"'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns hint with node ID only when title matches class type', () => {
|
||||
expect(buildMissingNodeHint('MyNodeClass', 'MyNodeClass', '42')).toBe(
|
||||
'Node ID #42'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns undefined when title matches class type and no node ID', () => {
|
||||
expect(
|
||||
buildMissingNodeHint('MyNodeClass', 'MyNodeClass', undefined)
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when title is null and no node ID', () => {
|
||||
expect(buildMissingNodeHint(null, 'MyNodeClass', undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns node ID hint when title is null but node ID exists', () => {
|
||||
expect(buildMissingNodeHint(null, 'MyNodeClass', '42')).toBe('Node ID #42')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createMissingNodeTypeFromError', () => {
|
||||
it('returns string type when no hint is generated', () => {
|
||||
const result = createMissingNodeTypeFromError({
|
||||
class_type: 'MyNodeClass',
|
||||
node_title: 'MyNodeClass'
|
||||
})
|
||||
expect(result).toBe('MyNodeClass')
|
||||
})
|
||||
|
||||
it('returns object with hint when title differs from class type', () => {
|
||||
const result = createMissingNodeTypeFromError({
|
||||
class_type: 'MyNodeClass',
|
||||
node_title: 'My Custom Title',
|
||||
node_id: '42'
|
||||
})
|
||||
expect(result).toEqual({
|
||||
type: 'MyNodeClass',
|
||||
hint: '"My Custom Title" (Node ID #42)'
|
||||
})
|
||||
})
|
||||
|
||||
it('handles null class_type by defaulting to Unknown', () => {
|
||||
const result = createMissingNodeTypeFromError({
|
||||
class_type: null,
|
||||
node_title: 'Some Title',
|
||||
node_id: '42'
|
||||
})
|
||||
expect(result).toEqual({
|
||||
type: 'Unknown',
|
||||
hint: '"Some Title" (Node ID #42)'
|
||||
})
|
||||
})
|
||||
|
||||
it('handles empty extra_info', () => {
|
||||
const result = createMissingNodeTypeFromError({})
|
||||
expect(result).toBe('Unknown')
|
||||
})
|
||||
|
||||
it('returns object with node ID hint when only node_id is available', () => {
|
||||
const result = createMissingNodeTypeFromError({
|
||||
class_type: 'MyNodeClass',
|
||||
node_id: '123'
|
||||
})
|
||||
expect(result).toEqual({
|
||||
type: 'MyNodeClass',
|
||||
hint: 'Node ID #123'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
import type { MissingNodeTypeExtraInfo } from '../types/missingNodeErrorTypes'
|
||||
|
||||
/**
|
||||
* Builds a hint string from missing node metadata.
|
||||
* Provides context about which node is missing (title, ID) when available.
|
||||
*/
|
||||
export function buildMissingNodeHint(
|
||||
nodeTitle: string | null | undefined,
|
||||
classType: string,
|
||||
nodeId: string | undefined
|
||||
): string | undefined {
|
||||
const hasTitle = nodeTitle && nodeTitle !== classType
|
||||
if (hasTitle && nodeId) {
|
||||
return `"${nodeTitle}" (Node ID #${nodeId})`
|
||||
} else if (hasTitle) {
|
||||
return `"${nodeTitle}"`
|
||||
} else if (nodeId) {
|
||||
return `Node ID #${nodeId}`
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MissingNodeType from backend error extra_info.
|
||||
* Used when the /prompt endpoint returns a missing_node_type error.
|
||||
*/
|
||||
export function createMissingNodeTypeFromError(
|
||||
extraInfo: MissingNodeTypeExtraInfo
|
||||
): MissingNodeType {
|
||||
const classType = extraInfo.class_type ?? 'Unknown'
|
||||
const nodeTitle = extraInfo.node_title ?? classType
|
||||
const hint = buildMissingNodeHint(nodeTitle, classType, extraInfo.node_id)
|
||||
|
||||
return hint ? { type: classType, hint } : classType
|
||||
}
|
||||
Reference in New Issue
Block a user