fix: comprehensive error handling and node-level properties validation (fixes #26)

Root cause: AI agents were placing error handling properties inside `parameters` instead of at node level

Major changes:
- Enhanced workflow validator to check for ALL node-level properties (expanded from 6 to 11)
- Added validation for onError property values and deprecation warnings for continueOnFail
- Updated all examples to use modern error handling (onError instead of continueOnFail)
- Added comprehensive node-level properties documentation in tools_documentation
- Enhanced MCP tool documentation for n8n_create_workflow and n8n_update_partial_workflow
- Added test script demonstrating correct node-level property usage

Node-level properties now validated:
- credentials, disabled, notes, notesInFlow, executeOnce
- onError, retryOnFail, maxTries, waitBetweenTries, alwaysOutputData
- continueOnFail (deprecated)

Validation improvements:
- Detects misplaced properties and provides clear fix examples
- Shows complete node structure when properties are incorrectly placed
- Type validation for all node-level boolean and string properties
- Smart error messages with correct placement guidance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-09 20:39:24 +02:00
parent 66e032c2a0
commit eab3cc858e
12 changed files with 1638 additions and 138 deletions

View File

@@ -20,7 +20,7 @@ export class NodeSpecificValidators {
* Validate Slack node configuration with operation awareness
*/
static validateSlack(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions } = context;
const { config, errors, warnings, suggestions, autofix } = context;
const { resource, operation } = config;
// Message operations
@@ -62,6 +62,30 @@ export class NodeSpecificValidators {
});
}
}
// Error handling for Slack operations
if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'Slack API can have rate limits and transient failures',
suggestion: 'Add onError: "continueRegularOutput" with retryOnFail for resilience'
});
autofix.onError = 'continueRegularOutput';
autofix.retryOnFail = true;
autofix.maxTries = 2;
autofix.waitBetweenTries = 3000; // Slack rate limits
}
// Check for deprecated continueOnFail
if (config.continueOnFail !== undefined) {
warnings.push({
type: 'deprecated',
property: 'continueOnFail',
message: 'continueOnFail is deprecated. Use onError instead',
suggestion: 'Replace with onError: "continueRegularOutput"'
});
}
}
private static validateSlackSendMessage(context: NodeValidationContext): void {
@@ -376,7 +400,7 @@ export class NodeSpecificValidators {
* Validate OpenAI node configuration
*/
static validateOpenAI(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions } = context;
const { config, errors, warnings, suggestions, autofix } = context;
const { resource, operation } = config;
if (resource === 'chat' && operation === 'create') {
@@ -433,13 +457,38 @@ export class NodeSpecificValidators {
}
}
}
// Error handling for AI API calls
if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'AI APIs have rate limits and can return errors',
suggestion: 'Add onError: "continueRegularOutput" with retryOnFail and longer wait times'
});
autofix.onError = 'continueRegularOutput';
autofix.retryOnFail = true;
autofix.maxTries = 3;
autofix.waitBetweenTries = 5000; // Longer wait for rate limits
autofix.alwaysOutputData = true;
}
// Check for deprecated continueOnFail
if (config.continueOnFail !== undefined) {
warnings.push({
type: 'deprecated',
property: 'continueOnFail',
message: 'continueOnFail is deprecated. Use onError instead',
suggestion: 'Replace with onError: "continueRegularOutput"'
});
}
}
/**
* Validate MongoDB node configuration
*/
static validateMongoDB(context: NodeValidationContext): void {
const { config, errors, warnings } = context;
const { config, errors, warnings, autofix } = context;
const { operation } = config;
// Collection is always required
@@ -501,91 +550,44 @@ export class NodeSpecificValidators {
}
break;
}
// Error handling for MongoDB operations
if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
if (operation === 'find') {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'MongoDB queries can fail due to connection issues',
suggestion: 'Add onError: "continueRegularOutput" with retryOnFail'
});
autofix.onError = 'continueRegularOutput';
autofix.retryOnFail = true;
autofix.maxTries = 3;
} else if (['insert', 'update', 'delete'].includes(operation)) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'MongoDB write operations should handle errors carefully',
suggestion: 'Add onError: "continueErrorOutput" to handle write failures separately'
});
autofix.onError = 'continueErrorOutput';
autofix.retryOnFail = true;
autofix.maxTries = 2;
autofix.waitBetweenTries = 1000;
}
}
// Check for deprecated continueOnFail
if (config.continueOnFail !== undefined) {
warnings.push({
type: 'deprecated',
property: 'continueOnFail',
message: 'continueOnFail is deprecated. Use onError instead',
suggestion: 'Replace with onError: "continueRegularOutput" or "continueErrorOutput"'
});
}
}
/**
* Validate Webhook node configuration
*/
static validateWebhook(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions } = context;
// Path validation
if (!config.path) {
errors.push({
type: 'missing_required',
property: 'path',
message: 'Webhook path is required',
fix: 'Set a unique path like "my-webhook" (no leading slash)'
});
} else {
const path = config.path;
// Check for leading slash
if (path.startsWith('/')) {
warnings.push({
type: 'inefficient',
property: 'path',
message: 'Webhook path should not start with /',
suggestion: 'Remove the leading slash: use "my-webhook" instead of "/my-webhook"'
});
}
// Check for spaces
if (path.includes(' ')) {
errors.push({
type: 'invalid_value',
property: 'path',
message: 'Webhook path cannot contain spaces',
fix: 'Replace spaces with hyphens or underscores'
});
}
// Check for special characters
if (!/^[a-zA-Z0-9\-_\/]+$/.test(path.replace(/^\//, ''))) {
warnings.push({
type: 'inefficient',
property: 'path',
message: 'Webhook path contains special characters',
suggestion: 'Use only letters, numbers, hyphens, and underscores'
});
}
}
// Response mode validation
if (config.responseMode === 'responseNode') {
suggestions.push('Add a "Respond to Webhook" node to send custom responses');
if (!config.responseData) {
warnings.push({
type: 'missing_common',
property: 'responseData',
message: 'Response data not configured for responseNode mode',
suggestion: 'Add a "Respond to Webhook" node or change responseMode'
});
}
}
// HTTP method validation
if (config.httpMethod && Array.isArray(config.httpMethod)) {
if (config.httpMethod.length === 0) {
errors.push({
type: 'invalid_value',
property: 'httpMethod',
message: 'At least one HTTP method must be selected',
fix: 'Select GET, POST, or other methods your webhook should accept'
});
}
}
// Authentication warnings
if (!config.authentication || config.authentication === 'none') {
warnings.push({
type: 'security',
message: 'Webhook has no authentication',
suggestion: 'Consider adding authentication to prevent unauthorized access'
});
}
}
/**
* Validate Postgres node configuration
@@ -677,6 +679,42 @@ export class NodeSpecificValidators {
if (config.connectionTimeout === undefined) {
suggestions.push('Consider setting connectionTimeout to handle slow connections');
}
// Error handling for database operations
if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
if (operation === 'execute' && config.query?.toLowerCase().includes('select')) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'Database reads can fail due to connection issues',
suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true'
});
autofix.onError = 'continueRegularOutput';
autofix.retryOnFail = true;
autofix.maxTries = 3;
} else if (['insert', 'update', 'delete'].includes(operation)) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'Database writes should handle errors carefully',
suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures'
});
autofix.onError = 'stopWorkflow';
autofix.retryOnFail = true;
autofix.maxTries = 2;
autofix.waitBetweenTries = 2000;
}
}
// Check for deprecated continueOnFail
if (config.continueOnFail !== undefined) {
warnings.push({
type: 'deprecated',
property: 'continueOnFail',
message: 'continueOnFail is deprecated. Use onError instead',
suggestion: 'Replace with onError: "continueRegularOutput" or "stopWorkflow"'
});
}
}
/**
@@ -751,6 +789,25 @@ export class NodeSpecificValidators {
if (config.timezone === undefined) {
suggestions.push('Consider setting timezone to ensure consistent date/time handling');
}
// Error handling for MySQL operations (similar to Postgres)
if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
if (operation === 'execute' && config.query?.toLowerCase().includes('select')) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'Database queries can fail due to connection issues',
suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true'
});
} else if (['insert', 'update', 'delete'].includes(operation)) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'Database modifications should handle errors carefully',
suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures'
});
}
}
}
/**
@@ -834,4 +891,169 @@ export class NodeSpecificValidators {
}
}
}
/**
* Validate HTTP Request node configuration with error handling awareness
*/
static validateHttpRequest(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions, autofix } = context;
const { method = 'GET', url, sendBody, authentication } = config;
// Basic URL validation
if (!url) {
errors.push({
type: 'missing_required',
property: 'url',
message: 'URL is required for HTTP requests',
fix: 'Provide the full URL including protocol (https://...)'
});
} else if (!url.startsWith('http://') && !url.startsWith('https://') && !url.includes('{{')) {
warnings.push({
type: 'invalid_value',
property: 'url',
message: 'URL should start with http:// or https://',
suggestion: 'Use https:// for secure connections'
});
}
// Method-specific validation
if (['POST', 'PUT', 'PATCH'].includes(method) && !sendBody) {
warnings.push({
type: 'missing_common',
property: 'sendBody',
message: `${method} requests typically include a body`,
suggestion: 'Set sendBody: true and configure the body content'
});
}
// Error handling recommendations
if (!config.retryOnFail && !config.onError && !config.continueOnFail) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'HTTP requests can fail due to network issues or server errors',
suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true for resilience'
});
// Auto-fix suggestion for error handling
autofix.onError = 'continueRegularOutput';
autofix.retryOnFail = true;
autofix.maxTries = 3;
autofix.waitBetweenTries = 1000;
}
// Check for deprecated continueOnFail
if (config.continueOnFail !== undefined) {
warnings.push({
type: 'deprecated',
property: 'continueOnFail',
message: 'continueOnFail is deprecated. Use onError instead',
suggestion: 'Replace with onError: "continueRegularOutput"'
});
autofix.onError = config.continueOnFail ? 'continueRegularOutput' : 'stopWorkflow';
delete autofix.continueOnFail;
}
// Check retry configuration
if (config.retryOnFail) {
// Validate retry settings
if (!['GET', 'HEAD', 'OPTIONS'].includes(method) && (!config.maxTries || config.maxTries > 3)) {
warnings.push({
type: 'best_practice',
property: 'maxTries',
message: `${method} requests might not be idempotent. Use fewer retries.`,
suggestion: 'Set maxTries: 2 for non-idempotent operations'
});
}
// Suggest alwaysOutputData for debugging
if (!config.alwaysOutputData) {
suggestions.push('Enable alwaysOutputData to capture error responses for debugging');
autofix.alwaysOutputData = true;
}
}
// Authentication warnings
if (url && url.includes('api') && !authentication) {
warnings.push({
type: 'security',
property: 'authentication',
message: 'API endpoints typically require authentication',
suggestion: 'Configure authentication method (Bearer token, API key, etc.)'
});
}
// Timeout recommendations
if (!config.timeout) {
suggestions.push('Consider setting a timeout to prevent hanging requests');
}
}
/**
* Validate Webhook node configuration with error handling
*/
static validateWebhook(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions, autofix } = context;
const { path, httpMethod = 'POST', responseMode } = config;
// Path validation
if (!path) {
errors.push({
type: 'missing_required',
property: 'path',
message: 'Webhook path is required',
fix: 'Provide a unique path like "my-webhook" or "github-events"'
});
} else if (path.startsWith('/')) {
warnings.push({
type: 'invalid_value',
property: 'path',
message: 'Webhook path should not start with /',
suggestion: 'Use "webhook-name" instead of "/webhook-name"'
});
}
// Error handling for webhooks
if (!config.onError && !config.continueOnFail) {
warnings.push({
type: 'best_practice',
property: 'onError',
message: 'Webhooks should always send a response, even on error',
suggestion: 'Set onError: "continueRegularOutput" to ensure webhook responses'
});
autofix.onError = 'continueRegularOutput';
}
// Check for deprecated continueOnFail in webhooks
if (config.continueOnFail !== undefined) {
warnings.push({
type: 'deprecated',
property: 'continueOnFail',
message: 'continueOnFail is deprecated. Use onError instead',
suggestion: 'Replace with onError: "continueRegularOutput"'
});
autofix.onError = 'continueRegularOutput';
delete autofix.continueOnFail;
}
// Response mode validation
if (responseMode === 'responseNode' && !config.onError && !config.continueOnFail) {
errors.push({
type: 'invalid_configuration',
property: 'responseMode',
message: 'responseNode mode requires onError: "continueRegularOutput"',
fix: 'Set onError to ensure response is always sent'
});
}
// Always output data for debugging
if (!config.alwaysOutputData) {
suggestions.push('Enable alwaysOutputData to debug webhook payloads');
autofix.alwaysOutputData = true;
}
// Security suggestions
suggestions.push('Consider adding webhook validation (HMAC signature verification)');
suggestions.push('Implement rate limiting for public webhooks');
}
}