Files
ComfyUI_frontend/src/utils/executionErrorUtil.ts
jaeone94 31a33a0ba2 feat: auto-resolve simple validation errors on widget change and slot connection (#9464)
## Summary

Automatically clears transient validation errors
(`value_bigger_than_max`, `value_smaller_than_min`, `value_not_in_list`,
`required_input_missing`) when the user modifies a widget value or
connects an input slot, so resolved errors don't linger in the error
panel. Also clears missing model state when the user changes a combo
widget value.

## Changes

- **`useNodeErrorAutoResolve` composable**: watches widget changes and
slot connections, clears matching errors via `executionErrorStore`
- **`executionErrorStore`**: adds `clearSimpleNodeErrors` and
`clearSimpleWidgetErrorIfValid` with granular per-slot error removal
- **`executionErrorUtil`**: adds `isValueStillOutOfRange` to prevent
premature clearing when a new value still violates the constraint
- **`graphTraversalUtil`**: adds `getExecutionIdFromNodeData` for
subgraph-aware execution ID resolution
- **`GraphCanvas.vue`**: fixes subgraph error key lookup by using
`getExecutionIdByNode` instead of raw `node.id`
- **`NodeWidgets.vue`**: wires up the new composable to the widget layer
- **`missingModelStore`**: adds `removeMissingModelByWidget` to clear
missing model state on widget value change
- **`useGraphNodeManager`**: registers composable per node
- **Tests**: 126 new unit tests covering error clearing, range
validation, and graph traversal edge cases

## Screenshots



https://github.com/user-attachments/assets/515ea811-ff84-482a-a866-a17e5c779c39



https://github.com/user-attachments/assets/a2b30f02-4929-4537-952c-a0febe20f02e


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9464-feat-auto-resolve-simple-validation-errors-on-widget-change-and-slot-connection-31b6d73d3650816b8afdc34f4b40295a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:49:44 +09:00

128 lines
3.9 KiB
TypeScript

import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { NodeError, PromptError } from '@/schemas/apiSchema'
/**
* The standard prompt validation response shape (`{ error, node_errors }`).
* In cloud, this is embedded as JSON inside `execution_error.exception_message`
* because prompts are queued asynchronously and errors arrive via WebSocket
* rather than as direct HTTP responses.
*/
interface CloudValidationError {
error?: { type?: string; message?: string; details?: string } | string
node_errors?: Record<NodeId, NodeError>
}
export function isCloudValidationError(
value: unknown
): value is CloudValidationError {
return (
value !== null &&
typeof value === 'object' &&
('error' in value || 'node_errors' in value)
)
}
/**
* Extracts a prompt validation response embedded in an exception message string.
*
* Cloud example `exception_message`:
* "Failed to send prompt request: ... 400: {\"error\":{...},\"node_errors\":{...}}"
*
* This function finds the first '{' and parses the trailing JSON.
*/
export function tryExtractValidationError(
exceptionMessage: string
): CloudValidationError | null {
const jsonStart = exceptionMessage.indexOf('{')
const jsonEnd = exceptionMessage.lastIndexOf('}')
if (jsonStart === -1 || jsonEnd === -1) return null
try {
const parsed: unknown = JSON.parse(
exceptionMessage.substring(jsonStart, jsonEnd + 1)
)
return isCloudValidationError(parsed) ? parsed : null
} catch {
return null
}
}
type CloudValidationResult =
| { kind: 'nodeErrors'; nodeErrors: Record<NodeId, NodeError> }
| { kind: 'promptError'; promptError: PromptError }
/**
* Classifies an embedded cloud validation error from `exception_message`
* as either node-level errors or a prompt-level error.
*
* Returns `null` if the message does not contain a recognizable validation error.
*/
export function classifyCloudValidationError(
exceptionMessage: string
): CloudValidationResult | null {
const extracted = tryExtractValidationError(exceptionMessage)
if (!extracted) return null
const { error, node_errors } = extracted
const hasNodeErrors = node_errors && Object.keys(node_errors).length > 0
if (hasNodeErrors) {
return { kind: 'nodeErrors', nodeErrors: node_errors }
}
if (error && typeof error === 'object') {
return {
kind: 'promptError',
promptError: {
type: error.type ?? 'error',
message: error.message ?? '',
details: error.details ?? ''
}
}
}
if (typeof error === 'string') {
return {
kind: 'promptError',
promptError: { type: 'error', message: error, details: '' }
}
}
return null
}
/**
* Error types that can be resolved automatically when the user changes a
* widget value or establishes a connection, without requiring a re-run.
*
* When adding new types, review {@link isValueStillOutOfRange} to ensure
* the new type does not require range validation before auto-clearing.
*/
export const SIMPLE_ERROR_TYPES = new Set([
'value_bigger_than_max',
'value_smaller_than_min',
'value_not_in_list',
'required_input_missing'
])
/**
* Returns true if `value` still violates a recorded range constraint.
* Pass errors already filtered to the target widget (by `input_name`).
* `options` should contain the widget's configured `min` / `max`.
*
* Returns true (keeps the error) when a bound is unknown (`undefined`).
*/
export function isValueStillOutOfRange(
value: number,
errors: NodeError['errors'],
options: { min?: number; max?: number }
): boolean {
const hasMaxError = errors.some((e) => e.type === 'value_bigger_than_max')
const hasMinError = errors.some((e) => e.type === 'value_smaller_than_min')
return (
(hasMaxError && (options.max === undefined || value > options.max)) ||
(hasMinError && (options.min === undefined || value < options.min))
)
}