From b8c76e10ff36667432fa127f97678dbfec78654c Mon Sep 17 00:00:00 2001 From: elijahr Date: Tue, 23 Sep 2025 02:47:50 -0500 Subject: [PATCH 1/3] Improve workflow validation error logging and display - Add zodErrorLogger utility for clean, structured Zod error logging - Enhanced workflow validation error messages with specific field names and node numbers - Improved .env.local loading in Vite config using loadEnv() - Simplified error handling in migration parsing - Single console.error per validation failure with complete error details - User-friendly toast notifications with truncated error summaries Fixes workflow validation error debugging for generated templates. --- invokeai/frontend/web/.tool-versions | 1 + .../web/src/common/util/zodErrorLogger.ts | 45 ++++++++++++++++++ .../nodes/util/workflow/migrations.ts | 2 +- .../hooks/useLoadWorkflowFromFile.tsx | 17 ++++++- .../hooks/useValidateAndLoadWorkflow.ts | 46 +++++++++++++++++-- invokeai/frontend/web/vite.config.mts | 17 +++++-- 6 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 invokeai/frontend/web/.tool-versions create mode 100644 invokeai/frontend/web/src/common/util/zodErrorLogger.ts diff --git a/invokeai/frontend/web/.tool-versions b/invokeai/frontend/web/.tool-versions new file mode 100644 index 00000000000..eebb231c446 --- /dev/null +++ b/invokeai/frontend/web/.tool-versions @@ -0,0 +1 @@ +nodejs 24.3.0 diff --git a/invokeai/frontend/web/src/common/util/zodErrorLogger.ts b/invokeai/frontend/web/src/common/util/zodErrorLogger.ts new file mode 100644 index 00000000000..7fe5ab6e2e0 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/zodErrorLogger.ts @@ -0,0 +1,45 @@ +import type { z } from 'zod'; + +interface ZodErrorLogOptions { + context?: string; +} + +/** + * Logs Zod validation errors with a simple description and complete error data. + */ +export const logZodError = (error: z.ZodError, options: ZodErrorLogOptions = {}): void => { + const { context = 'Validation' } = options; + + // eslint-disable-next-line no-console + console.error(`${context} failed with ${error.issues.length} errors:`, { + summary: error.issues.map((issue) => `${issue.path.join('.') || 'root'}: ${issue.message}`), + issues: error.issues, + }); +}; + +/** + * Creates a human-readable summary of Zod errors suitable for user-facing messages. + * Limits the number of errors shown and provides a count of remaining errors. + */ +export const createZodErrorSummary = ( + error: z.ZodError, + maxErrors: number = 10, + pathFormatter?: (path: string, message: string) => string +): string => { + const defaultFormatter = (path: string, message: string) => `${path || 'Root'}: ${message}`; + + const formatter = pathFormatter || defaultFormatter; + + const errors = error.issues.map((issue) => formatter(issue.path.join('.'), issue.message)); + + const visibleErrors = errors.slice(0, maxErrors); + const remainingCount = errors.length - maxErrors; + + let summary = visibleErrors.join('; '); + + if (remainingCount > 0) { + summary += ` (+${remainingCount} more error${remainingCount === 1 ? '' : 's'})`; + } + + return summary; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index 32971a02d0b..896113f502f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -1,3 +1,4 @@ +import { logZodError } from 'common/util/zodErrorLogger'; import { deepClone } from 'common/util/deepClone'; import { forEach, get } from 'es-toolkit/compat'; import { $templates } from 'features/nodes/store/nodesSlice'; @@ -102,6 +103,5 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => { // We should now have a V3 workflow const migratedWorkflow = zWorkflowV3.parse(workflow); - return migratedWorkflow; }; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx index c3c0849ed10..26a3c3f7153 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx @@ -39,8 +39,21 @@ export const useLoadWorkflowFromFile = () => { dispatch(workflowLoadedFromFile()); onSuccess?.(validatedWorkflow); resolve(validatedWorkflow); - } catch { - // This is catching the error from the parsing the JSON file + } catch (e) { + // Log file parsing/loading errors + if (e instanceof SyntaxError) { + // eslint-disable-next-line no-console + console.error('Workflow file JSON parsing failed:', { + fileName: file.name, + error: e.message, + }); + } else { + // eslint-disable-next-line no-console + console.error('Workflow file loading failed:', { + fileName: file.name, + error: e, + }); + } onError?.(); reject(); } finally { diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts index 39d5a9cd917..39202350da7 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import { useAppDispatch } from 'app/store/storeHooks'; +import { createZodErrorSummary, logZodError } from 'common/util/zodErrorLogger'; import { getIsFormEmpty } from 'features/nodes/components/sidePanel/builder/form-manipulation'; import { $nodeExecutionStates } from 'features/nodes/hooks/useNodeExecutionState'; import { $templates, workflowLoaded } from 'features/nodes/store/nodesSlice'; @@ -16,7 +17,6 @@ import { useCallback } from 'react'; import { serializeError } from 'serialize-error'; import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks'; import { z } from 'zod'; -import { fromZodError } from 'zod-validation-error/v4'; const log = logger('workflows'); @@ -120,10 +120,46 @@ export const useValidateAndLoadWorkflow = () => { }); } else if (e instanceof z.ZodError) { // There was a problem validating the workflow itself - const { message } = fromZodError(e, { - prefix: t('nodes.workflowValidation'), - }); - log.error({ error: serializeError(e) }, message); + + // Log comprehensive error information for developers + logZodError(e, { context: 'Workflow validation' }); + + // Create human-readable message for toast with node-specific formatting + const pathFormatter = (path: string, message: string) => { + const nodeMatch = path.match(/nodes\.(\d+)\.data\.inputs\.([^.]+)(?:\.(.+))?/); + const nodeDataMatch = path.match(/nodes\.(\d+)\.data\.([^.]+)/); + const nodeMatch2 = path.match(/nodes\.(\d+)\.([^.]+)/); + + if (nodeMatch) { + const [, nodeIndex, fieldName, subField] = nodeMatch; + const fieldPath = subField ? `${fieldName}.${subField}` : fieldName; + return `Node ${parseInt(nodeIndex, 10) + 1} input "${fieldPath}": ${message}`; + } else if (nodeDataMatch) { + const [, nodeIndex, fieldName] = nodeDataMatch; + return `Node ${parseInt(nodeIndex, 10) + 1} data "${fieldName}": ${message}`; + } else if (nodeMatch2) { + const [, nodeIndex, fieldName] = nodeMatch2; + return `Node ${parseInt(nodeIndex, 10) + 1} "${fieldName}": ${message}`; + } else if (path.startsWith('nodes.')) { + const pathParts = path.split('.'); + const nodeIndex = pathParts[1]; + const remainingPath = pathParts.slice(2).join('.'); + return `Node ${parseInt(nodeIndex, 10) + 1} (${remainingPath || 'general'}): ${message}`; + } else { + return `${path || 'Workflow'}: ${message}`; + } + }; + + const errorSummary = createZodErrorSummary(e, 10, pathFormatter); + const message = `${t('nodes.workflowValidation')}: ${errorSummary}`; + + log.error( + { + error: serializeError(e), + totalIssues: e.issues.length, + }, + `Workflow validation failed with ${e.issues.length} issues: ${message}` + ); toast({ id: 'UNABLE_TO_VALIDATE_WORKFLOW', title: t('nodes.unableToValidateWorkflow'), diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index a697148322d..fa11de36353 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react-swc'; import path from 'path'; import { visualizer } from 'rollup-plugin-visualizer'; import type { PluginOption } from 'vite'; -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; import dts from 'vite-plugin-dts'; import eslint from 'vite-plugin-eslint'; @@ -11,6 +11,15 @@ import tsconfigPaths from 'vite-tsconfig-paths'; import { loggerContextPlugin } from './vite-plugin-logger-context'; export default defineConfig(({ mode }) => { + // Load env file based on mode in the current working directory. + // Set the third parameter to '' to load all env regardless of the VITE_ prefix. + const env = loadEnv(mode, process.cwd(), ''); + + // Get InvokeAI API base URL from environment variable, default to localhost + const apiBaseUrl = env.INVOKEAI_API_BASE_URL || 'http://127.0.0.1:9090'; + const apiHost = new URL(apiBaseUrl).host; + const wsProtocol = apiBaseUrl.startsWith('https') ? 'wss' : 'ws'; + if (mode === 'package') { return { base: './', @@ -81,16 +90,16 @@ export default defineConfig(({ mode }) => { server: { proxy: { '/ws/socket.io': { - target: 'ws://127.0.0.1:9090', + target: `${wsProtocol}://${apiHost}`, ws: true, }, '/openapi.json': { - target: 'http://127.0.0.1:9090/openapi.json', + target: `${apiBaseUrl}/openapi.json`, rewrite: (path) => path.replace(/^\/openapi.json/, ''), changeOrigin: true, }, '/api/': { - target: 'http://127.0.0.1:9090/api/', + target: `${apiBaseUrl}/api/`, rewrite: (path) => path.replace(/^\/api/, ''), changeOrigin: true, }, From 830f1971f4d1fc9f985916744963aa1ec8f9a334 Mon Sep 17 00:00:00 2001 From: elijahr Date: Tue, 23 Sep 2025 02:50:28 -0500 Subject: [PATCH 2/3] Delete invokeai/frontend/web/.tool-versions --- invokeai/frontend/web/.tool-versions | 1 - 1 file changed, 1 deletion(-) delete mode 100644 invokeai/frontend/web/.tool-versions diff --git a/invokeai/frontend/web/.tool-versions b/invokeai/frontend/web/.tool-versions deleted file mode 100644 index eebb231c446..00000000000 --- a/invokeai/frontend/web/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -nodejs 24.3.0 From dd14c64cd6bad8faf1ff8514f146e522c093dc27 Mon Sep 17 00:00:00 2001 From: elijahr Date: Tue, 23 Sep 2025 02:52:11 -0500 Subject: [PATCH 3/3] Revert invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts --- .../frontend/web/src/features/nodes/util/workflow/migrations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index 896113f502f..32971a02d0b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -1,4 +1,3 @@ -import { logZodError } from 'common/util/zodErrorLogger'; import { deepClone } from 'common/util/deepClone'; import { forEach, get } from 'es-toolkit/compat'; import { $templates } from 'features/nodes/store/nodesSlice'; @@ -103,5 +102,6 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => { // We should now have a V3 workflow const migratedWorkflow = zWorkflowV3.parse(workflow); + return migratedWorkflow; };