Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions invokeai/frontend/web/src/common/util/zodErrorLogger.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Comment on lines +42 to +56
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use the app logger here.

onError?.();
reject();
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');

Expand Down Expand Up @@ -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'),
Expand Down
17 changes: 13 additions & 4 deletions invokeai/frontend/web/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@ 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';
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: './',
Expand Down Expand Up @@ -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,
},
Expand Down