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:
@@ -16,14 +16,14 @@ export interface ValidationResult {
|
||||
}
|
||||
|
||||
export interface ValidationError {
|
||||
type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible';
|
||||
type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible' | 'invalid_configuration';
|
||||
property: string;
|
||||
message: string;
|
||||
fix?: string;
|
||||
}
|
||||
|
||||
export interface ValidationWarning {
|
||||
type: 'missing_common' | 'deprecated' | 'inefficient' | 'security';
|
||||
type: 'missing_common' | 'deprecated' | 'inefficient' | 'security' | 'best_practice' | 'invalid_value';
|
||||
property?: string;
|
||||
message: string;
|
||||
suggestion?: string;
|
||||
|
||||
@@ -491,9 +491,11 @@ export class EnhancedConfigValidator extends ConfigValidator {
|
||||
case 'strict':
|
||||
// Keep everything, add more suggestions
|
||||
if (result.warnings.length === 0 && result.errors.length === 0) {
|
||||
result.suggestions.push('Consider adding error handling and timeout configuration');
|
||||
result.suggestions.push('Consider adding error handling with onError property and timeout configuration');
|
||||
result.suggestions.push('Add authentication if connecting to external services');
|
||||
}
|
||||
// Require error handling for external service nodes
|
||||
this.enforceErrorHandlingForProfile(result, profile);
|
||||
break;
|
||||
|
||||
case 'ai-friendly':
|
||||
@@ -503,7 +505,64 @@ export class EnhancedConfigValidator extends ConfigValidator {
|
||||
result.warnings = result.warnings.filter(w =>
|
||||
w.type !== 'inefficient' || !w.property?.startsWith('_')
|
||||
);
|
||||
// Add error handling suggestions for AI-friendly profile
|
||||
this.addErrorHandlingSuggestions(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce error handling requirements based on profile
|
||||
*/
|
||||
private static enforceErrorHandlingForProfile(
|
||||
result: EnhancedValidationResult,
|
||||
profile: ValidationProfile
|
||||
): void {
|
||||
// Only enforce for strict profile on external service nodes
|
||||
if (profile !== 'strict') return;
|
||||
|
||||
const nodeType = result.operation?.resource || '';
|
||||
const errorProneTypes = ['httpRequest', 'webhook', 'database', 'api', 'slack', 'email', 'openai'];
|
||||
|
||||
if (errorProneTypes.some(type => nodeType.toLowerCase().includes(type))) {
|
||||
// Add general warning for strict profile
|
||||
// The actual error handling validation is done in node-specific validators
|
||||
result.warnings.push({
|
||||
type: 'best_practice',
|
||||
property: 'errorHandling',
|
||||
message: 'External service nodes should have error handling configured',
|
||||
suggestion: 'Add onError: "continueRegularOutput" or "stopWorkflow" with retryOnFail: true for resilience'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add error handling suggestions for AI-friendly profile
|
||||
*/
|
||||
private static addErrorHandlingSuggestions(
|
||||
result: EnhancedValidationResult
|
||||
): void {
|
||||
// Check if there are any network/API related errors
|
||||
const hasNetworkErrors = result.errors.some(e =>
|
||||
e.message.toLowerCase().includes('url') ||
|
||||
e.message.toLowerCase().includes('endpoint') ||
|
||||
e.message.toLowerCase().includes('api')
|
||||
);
|
||||
|
||||
if (hasNetworkErrors) {
|
||||
result.suggestions.push(
|
||||
'For API calls, consider adding onError: "continueRegularOutput" with retryOnFail: true and maxTries: 3'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for webhook configurations
|
||||
const isWebhook = result.operation?.resource === 'webhook' ||
|
||||
result.errors.some(e => e.message.toLowerCase().includes('webhook'));
|
||||
|
||||
if (isWebhook) {
|
||||
result.suggestions.push(
|
||||
'Webhooks should use onError: "continueRegularOutput" to ensure responses are always sent'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,13 @@ export class ExampleGenerator {
|
||||
sendBody: true,
|
||||
contentType: 'json',
|
||||
specifyBody: 'json',
|
||||
jsonBody: '{\n "action": "update",\n "data": {}\n}'
|
||||
jsonBody: '{\n "action": "update",\n "data": {}\n}',
|
||||
// Error handling for API calls
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 1000,
|
||||
alwaysOutputData: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -62,7 +68,10 @@ export class ExampleGenerator {
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'lastNode',
|
||||
responseData: 'allEntries',
|
||||
responseCode: 200
|
||||
responseCode: 200,
|
||||
// Webhooks should continue on fail to avoid blocking responses
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -220,7 +229,12 @@ DO UPDATE SET
|
||||
RETURNING *;`,
|
||||
additionalFields: {
|
||||
queryParams: '={{ $json.name }},{{ $json.email }},active'
|
||||
}
|
||||
},
|
||||
// Database operations should retry on connection errors
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 2000,
|
||||
onError: 'continueErrorOutput'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -258,7 +272,13 @@ RETURNING *;`,
|
||||
options: {
|
||||
maxTokens: 150,
|
||||
temperature: 0.7
|
||||
}
|
||||
},
|
||||
// AI calls should handle rate limits and transient errors
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 5000,
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -325,7 +345,12 @@ RETURNING *;`,
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
// Messaging services should handle rate limits
|
||||
retryOnFail: true,
|
||||
maxTries: 2,
|
||||
waitBetweenTries: 3000,
|
||||
onError: 'continueRegularOutput'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -347,7 +372,12 @@ RETURNING *;`,
|
||||
<p>Best regards,<br>The Team</p>`,
|
||||
options: {
|
||||
ccEmail: 'admin@company.com'
|
||||
}
|
||||
},
|
||||
// Email sending should handle transient failures
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 2000,
|
||||
onError: 'continueRegularOutput'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -427,7 +457,12 @@ return processedItems;`
|
||||
options: {
|
||||
upsert: true,
|
||||
returnNewDocument: true
|
||||
}
|
||||
},
|
||||
// NoSQL operations should handle connection issues
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 1000,
|
||||
onError: 'continueErrorOutput'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -443,7 +478,12 @@ return processedItems;`
|
||||
columns: 'customer_id,product_id,quantity,order_date',
|
||||
options: {
|
||||
queryBatching: 'independently'
|
||||
}
|
||||
},
|
||||
// Database writes should handle connection errors
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 2000,
|
||||
onError: 'stopWorkflow'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -514,6 +554,145 @@ return processedItems;`
|
||||
assignees: ['maintainer'],
|
||||
labels: ['bug', 'needs-triage']
|
||||
}
|
||||
},
|
||||
|
||||
// Error Handling Examples and Patterns
|
||||
'error-handling.modern-patterns': {
|
||||
minimal: {
|
||||
// Basic error handling - continue on error
|
||||
onError: 'continueRegularOutput'
|
||||
},
|
||||
common: {
|
||||
// Use error output for special handling
|
||||
onError: 'continueErrorOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
advanced: {
|
||||
// Stop workflow on critical errors
|
||||
onError: 'stopWorkflow',
|
||||
// But retry first
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 2000
|
||||
}
|
||||
},
|
||||
|
||||
'error-handling.api-with-retry': {
|
||||
minimal: {
|
||||
url: 'https://api.example.com/data',
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 1000
|
||||
},
|
||||
common: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users/{{ $json.userId }}',
|
||||
retryOnFail: true,
|
||||
maxTries: 5,
|
||||
waitBetweenTries: 2000,
|
||||
alwaysOutputData: true,
|
||||
// Headers for better debugging
|
||||
sendHeaders: true,
|
||||
headerParameters: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'X-Request-ID',
|
||||
value: '={{ $workflow.id }}-{{ $execution.id }}'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
advanced: {
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/critical-operation',
|
||||
sendBody: true,
|
||||
contentType: 'json',
|
||||
specifyBody: 'json',
|
||||
jsonBody: '{{ JSON.stringify($json) }}',
|
||||
// Exponential backoff pattern
|
||||
retryOnFail: true,
|
||||
maxTries: 5,
|
||||
waitBetweenTries: 1000,
|
||||
// Always output for debugging
|
||||
alwaysOutputData: true,
|
||||
// Stop workflow on error for critical operations
|
||||
onError: 'stopWorkflow'
|
||||
}
|
||||
},
|
||||
|
||||
'error-handling.fault-tolerant': {
|
||||
minimal: {
|
||||
// For non-critical operations
|
||||
onError: 'continueRegularOutput'
|
||||
},
|
||||
common: {
|
||||
// Data processing that shouldn't stop the workflow
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
advanced: {
|
||||
// Combination for resilient processing
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 2,
|
||||
waitBetweenTries: 500,
|
||||
alwaysOutputData: true
|
||||
}
|
||||
},
|
||||
|
||||
'error-handling.database-patterns': {
|
||||
minimal: {
|
||||
// Database reads can continue on error
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
common: {
|
||||
// Database writes should retry then stop
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 2000,
|
||||
onError: 'stopWorkflow'
|
||||
},
|
||||
advanced: {
|
||||
// Transaction-safe operations
|
||||
onError: 'continueErrorOutput',
|
||||
retryOnFail: false, // Don't retry transactions
|
||||
alwaysOutputData: true
|
||||
}
|
||||
},
|
||||
|
||||
'error-handling.webhook-patterns': {
|
||||
minimal: {
|
||||
// Always respond to webhooks
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
common: {
|
||||
// Process errors separately
|
||||
onError: 'continueErrorOutput',
|
||||
alwaysOutputData: true,
|
||||
// Add custom error response
|
||||
responseCode: 200,
|
||||
responseData: 'allEntries'
|
||||
}
|
||||
},
|
||||
|
||||
'error-handling.ai-patterns': {
|
||||
minimal: {
|
||||
// AI calls should handle rate limits
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 5000,
|
||||
onError: 'continueRegularOutput'
|
||||
},
|
||||
common: {
|
||||
// Exponential backoff for rate limits
|
||||
retryOnFail: true,
|
||||
maxTries: 5,
|
||||
waitBetweenTries: 2000,
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,13 @@ export class TaskTemplates {
|
||||
configuration: {
|
||||
method: 'GET',
|
||||
url: '',
|
||||
authentication: 'none'
|
||||
authentication: 'none',
|
||||
// Default error handling for API calls
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 1000,
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -52,6 +58,11 @@ export class TaskTemplates {
|
||||
property: 'sendHeaders',
|
||||
description: 'Add custom headers if needed',
|
||||
when: 'API requires specific headers'
|
||||
},
|
||||
{
|
||||
property: 'alwaysOutputData',
|
||||
description: 'Set to true to capture error responses',
|
||||
when: 'Need to debug API errors'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -66,7 +77,13 @@ export class TaskTemplates {
|
||||
sendBody: true,
|
||||
contentType: 'json',
|
||||
specifyBody: 'json',
|
||||
jsonBody: ''
|
||||
jsonBody: '',
|
||||
// POST requests might modify data, so be careful with retries
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 2,
|
||||
waitBetweenTries: 1000,
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -84,11 +101,17 @@ export class TaskTemplates {
|
||||
{
|
||||
property: 'authentication',
|
||||
description: 'Add authentication if required'
|
||||
},
|
||||
{
|
||||
property: 'onError',
|
||||
description: 'Set to "continueRegularOutput" for non-critical operations',
|
||||
when: 'Failure should not stop the workflow'
|
||||
}
|
||||
],
|
||||
notes: [
|
||||
'Make sure jsonBody contains valid JSON',
|
||||
'Content-Type header is automatically set to application/json'
|
||||
'Content-Type header is automatically set to application/json',
|
||||
'Be careful with retries on non-idempotent operations'
|
||||
]
|
||||
},
|
||||
|
||||
@@ -102,6 +125,12 @@ export class TaskTemplates {
|
||||
authentication: 'genericCredentialType',
|
||||
genericAuthType: 'headerAuth',
|
||||
sendHeaders: true,
|
||||
// Authentication calls should handle auth failures gracefully
|
||||
onError: 'continueErrorOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 2000,
|
||||
alwaysOutputData: true,
|
||||
headerParameters: {
|
||||
parameters: [
|
||||
{
|
||||
@@ -144,7 +173,10 @@ export class TaskTemplates {
|
||||
httpMethod: 'POST',
|
||||
path: 'webhook',
|
||||
responseMode: 'lastNode',
|
||||
responseData: 'allEntries'
|
||||
responseData: 'allEntries',
|
||||
// Webhooks should always respond, even on error
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -178,7 +210,10 @@ export class TaskTemplates {
|
||||
path: 'webhook',
|
||||
responseMode: 'responseNode',
|
||||
responseData: 'firstEntryJson',
|
||||
responseCode: 200
|
||||
responseCode: 200,
|
||||
// Ensure webhook always sends response
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -199,7 +234,12 @@ export class TaskTemplates {
|
||||
nodeType: 'nodes-base.postgres',
|
||||
configuration: {
|
||||
operation: 'executeQuery',
|
||||
query: ''
|
||||
query: '',
|
||||
// Database reads can continue on error
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 1000
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -229,7 +269,12 @@ export class TaskTemplates {
|
||||
operation: 'insert',
|
||||
table: '',
|
||||
columns: '',
|
||||
returnFields: '*'
|
||||
returnFields: '*',
|
||||
// Database writes should stop on error by default
|
||||
onError: 'stopWorkflow',
|
||||
retryOnFail: true,
|
||||
maxTries: 2,
|
||||
waitBetweenTries: 1000
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -265,7 +310,13 @@ export class TaskTemplates {
|
||||
content: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// AI calls should handle rate limits and API errors
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 5000,
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -393,7 +444,12 @@ return results;`
|
||||
resource: 'message',
|
||||
operation: 'post',
|
||||
channel: '',
|
||||
text: ''
|
||||
text: '',
|
||||
// Messaging can continue on error
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 2,
|
||||
waitBetweenTries: 2000
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -427,7 +483,13 @@ return results;`
|
||||
fromEmail: '',
|
||||
toEmail: '',
|
||||
subject: '',
|
||||
text: ''
|
||||
text: '',
|
||||
// Email sending should retry on transient failures
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 3000,
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
@@ -562,6 +624,255 @@ return results;`
|
||||
'Test each tool individually before combining',
|
||||
'Set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true for community nodes'
|
||||
]
|
||||
},
|
||||
|
||||
// Error Handling Templates
|
||||
'api_call_with_retry': {
|
||||
task: 'api_call_with_retry',
|
||||
description: 'Resilient API call with automatic retry on failure',
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
configuration: {
|
||||
method: 'GET',
|
||||
url: '',
|
||||
// Retry configuration for transient failures
|
||||
retryOnFail: true,
|
||||
maxTries: 5,
|
||||
waitBetweenTries: 2000,
|
||||
// Always capture response for debugging
|
||||
alwaysOutputData: true,
|
||||
// Add request tracking
|
||||
sendHeaders: true,
|
||||
headerParameters: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'X-Request-ID',
|
||||
value: '={{ $workflow.id }}-{{ $itemIndex }}'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
property: 'url',
|
||||
description: 'The API endpoint to call',
|
||||
example: 'https://api.example.com/resource/{{ $json.id }}'
|
||||
}
|
||||
],
|
||||
optionalEnhancements: [
|
||||
{
|
||||
property: 'authentication',
|
||||
description: 'Add API authentication'
|
||||
},
|
||||
{
|
||||
property: 'onError',
|
||||
description: 'Change to "stopWorkflow" for critical API calls',
|
||||
when: 'This is a critical API call that must succeed'
|
||||
}
|
||||
],
|
||||
notes: [
|
||||
'Retries help with rate limits and transient network issues',
|
||||
'waitBetweenTries prevents hammering the API',
|
||||
'alwaysOutputData captures error responses for debugging',
|
||||
'Consider exponential backoff for production use'
|
||||
]
|
||||
},
|
||||
|
||||
'fault_tolerant_processing': {
|
||||
task: 'fault_tolerant_processing',
|
||||
description: 'Data processing that continues despite individual item failures',
|
||||
nodeType: 'nodes-base.code',
|
||||
configuration: {
|
||||
language: 'javaScript',
|
||||
jsCode: `// Process items with error handling
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
try {
|
||||
// Your processing logic here
|
||||
const processed = {
|
||||
...item.json,
|
||||
processed: true,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
results.push({ json: processed });
|
||||
} catch (error) {
|
||||
// Log error but continue processing
|
||||
console.error('Processing failed for item:', item.json.id, error);
|
||||
|
||||
// Add error item to results
|
||||
results.push({
|
||||
json: {
|
||||
...item.json,
|
||||
error: error.message,
|
||||
processed: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;`,
|
||||
// Continue workflow even if code fails entirely
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
property: 'Processing logic',
|
||||
description: 'Replace the comment with your data transformation logic'
|
||||
}
|
||||
],
|
||||
optionalEnhancements: [
|
||||
{
|
||||
property: 'Error notification',
|
||||
description: 'Add IF node after to handle error items separately'
|
||||
}
|
||||
],
|
||||
notes: [
|
||||
'Individual item failures won\'t stop processing of other items',
|
||||
'Error items are marked and can be handled separately',
|
||||
'continueOnFail ensures workflow continues even on total failure'
|
||||
]
|
||||
},
|
||||
|
||||
'webhook_with_error_handling': {
|
||||
task: 'webhook_with_error_handling',
|
||||
description: 'Webhook that gracefully handles processing errors',
|
||||
nodeType: 'nodes-base.webhook',
|
||||
configuration: {
|
||||
httpMethod: 'POST',
|
||||
path: 'resilient-webhook',
|
||||
responseMode: 'responseNode',
|
||||
responseData: 'firstEntryJson',
|
||||
// Always continue to ensure response is sent
|
||||
onError: 'continueRegularOutput',
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
property: 'path',
|
||||
description: 'Unique webhook path',
|
||||
example: 'order-processor'
|
||||
},
|
||||
{
|
||||
property: 'Respond to Webhook node',
|
||||
description: 'Add node to send appropriate success/error responses'
|
||||
}
|
||||
],
|
||||
optionalEnhancements: [
|
||||
{
|
||||
property: 'Validation',
|
||||
description: 'Add IF node to validate webhook payload'
|
||||
},
|
||||
{
|
||||
property: 'Error logging',
|
||||
description: 'Add error handler node for failed requests'
|
||||
}
|
||||
],
|
||||
notes: [
|
||||
'onError: continueRegularOutput ensures webhook always sends a response',
|
||||
'Use Respond to Webhook node to send appropriate status codes',
|
||||
'Log errors but don\'t expose internal errors to webhook callers',
|
||||
'Consider rate limiting for public webhooks'
|
||||
]
|
||||
},
|
||||
|
||||
// Modern Error Handling Patterns
|
||||
'modern_error_handling_patterns': {
|
||||
task: 'modern_error_handling_patterns',
|
||||
description: 'Examples of modern error handling using onError property',
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
configuration: {
|
||||
method: 'GET',
|
||||
url: '',
|
||||
// Modern error handling approach
|
||||
onError: 'continueRegularOutput', // Options: continueRegularOutput, continueErrorOutput, stopWorkflow
|
||||
retryOnFail: true,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 2000,
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
property: 'url',
|
||||
description: 'The API endpoint'
|
||||
},
|
||||
{
|
||||
property: 'onError',
|
||||
description: 'Choose error handling strategy',
|
||||
example: 'continueRegularOutput'
|
||||
}
|
||||
],
|
||||
notes: [
|
||||
'onError replaces the deprecated continueOnFail property',
|
||||
'continueRegularOutput: Continue with normal output on error',
|
||||
'continueErrorOutput: Route errors to error output for special handling',
|
||||
'stopWorkflow: Stop the entire workflow on error',
|
||||
'Combine with retryOnFail for resilient workflows'
|
||||
]
|
||||
},
|
||||
|
||||
'database_transaction_safety': {
|
||||
task: 'database_transaction_safety',
|
||||
description: 'Database operations with proper error handling',
|
||||
nodeType: 'nodes-base.postgres',
|
||||
configuration: {
|
||||
operation: 'executeQuery',
|
||||
query: 'BEGIN; INSERT INTO orders ...; COMMIT;',
|
||||
// For transactions, don\'t retry automatically
|
||||
onError: 'continueErrorOutput',
|
||||
retryOnFail: false,
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
property: 'query',
|
||||
description: 'Your SQL query or transaction'
|
||||
}
|
||||
],
|
||||
notes: [
|
||||
'Transactions should not be retried automatically',
|
||||
'Use continueErrorOutput to handle errors separately',
|
||||
'Consider implementing compensating transactions',
|
||||
'Always log transaction failures for audit'
|
||||
]
|
||||
},
|
||||
|
||||
'ai_rate_limit_handling': {
|
||||
task: 'ai_rate_limit_handling',
|
||||
description: 'AI API calls with rate limit handling',
|
||||
nodeType: 'nodes-base.openAi',
|
||||
configuration: {
|
||||
resource: 'chat',
|
||||
operation: 'message',
|
||||
modelId: 'gpt-4',
|
||||
messages: {
|
||||
values: [
|
||||
{
|
||||
role: 'user',
|
||||
content: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
// Handle rate limits with exponential backoff
|
||||
onError: 'continueRegularOutput',
|
||||
retryOnFail: true,
|
||||
maxTries: 5,
|
||||
waitBetweenTries: 5000,
|
||||
alwaysOutputData: true
|
||||
},
|
||||
userMustProvide: [
|
||||
{
|
||||
property: 'messages.values[0].content',
|
||||
description: 'The prompt for the AI'
|
||||
}
|
||||
],
|
||||
notes: [
|
||||
'AI APIs often have rate limits',
|
||||
'Longer wait times help avoid hitting limits',
|
||||
'Consider implementing exponential backoff in Code node',
|
||||
'Monitor usage to stay within quotas'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -588,6 +899,13 @@ return results;`
|
||||
return this.templates[task];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific task template (alias for getTaskTemplate)
|
||||
*/
|
||||
static getTemplate(task: string): TaskTemplate | undefined {
|
||||
return this.getTaskTemplate(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for tasks by keyword
|
||||
*/
|
||||
@@ -607,13 +925,14 @@ return results;`
|
||||
*/
|
||||
static getTaskCategories(): Record<string, string[]> {
|
||||
return {
|
||||
'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth'],
|
||||
'Webhooks': ['receive_webhook', 'webhook_with_response'],
|
||||
'Database': ['query_postgres', 'insert_postgres_data'],
|
||||
'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent'],
|
||||
'Data Processing': ['transform_data', 'filter_data'],
|
||||
'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth', 'api_call_with_retry'],
|
||||
'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling'],
|
||||
'Database': ['query_postgres', 'insert_postgres_data', 'database_transaction_safety'],
|
||||
'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent', 'ai_rate_limit_handling'],
|
||||
'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing'],
|
||||
'Communication': ['send_slack_message', 'send_email'],
|
||||
'AI Tool Usage': ['use_google_sheets_as_tool', 'use_slack_as_tool', 'multi_tool_ai_agent']
|
||||
'AI Tool Usage': ['use_google_sheets_as_tool', 'use_slack_as_tool', 'multi_tool_ai_agent'],
|
||||
'Error Handling': ['modern_error_handling_patterns', 'api_call_with_retry', 'fault_tolerant_processing', 'webhook_with_error_handling', 'database_transaction_safety', 'ai_rate_limit_handling']
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -404,6 +404,7 @@ export class WorkflowDiffEngine {
|
||||
notes: operation.node.notes,
|
||||
notesInFlow: operation.node.notesInFlow,
|
||||
continueOnFail: operation.node.continueOnFail,
|
||||
onError: operation.node.onError,
|
||||
retryOnFail: operation.node.retryOnFail,
|
||||
maxTries: operation.node.maxTries,
|
||||
waitBetweenTries: operation.node.waitBetweenTries,
|
||||
|
||||
@@ -19,7 +19,15 @@ interface WorkflowNode {
|
||||
credentials?: any;
|
||||
disabled?: boolean;
|
||||
notes?: string;
|
||||
notesInFlow?: boolean;
|
||||
typeVersion?: number;
|
||||
continueOnFail?: boolean;
|
||||
onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow';
|
||||
retryOnFail?: boolean;
|
||||
maxTries?: number;
|
||||
waitBetweenTries?: number;
|
||||
alwaysOutputData?: boolean;
|
||||
executeOnce?: boolean;
|
||||
}
|
||||
|
||||
interface WorkflowConnection {
|
||||
@@ -775,6 +783,9 @@ export class WorkflowValidator {
|
||||
});
|
||||
}
|
||||
|
||||
// Check node-level error handling properties
|
||||
this.checkNodeErrorHandling(workflow, result);
|
||||
|
||||
// Check for very long linear workflows
|
||||
const linearChainLength = this.getLongestLinearChain(workflow);
|
||||
if (linearChainLength > 10) {
|
||||
@@ -1004,4 +1015,333 @@ export class WorkflowValidator {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check node-level error handling configuration
|
||||
*/
|
||||
private checkNodeErrorHandling(
|
||||
workflow: WorkflowJson,
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
// Define node types that typically interact with external services
|
||||
const errorProneNodeTypes = [
|
||||
'httpRequest',
|
||||
'webhook',
|
||||
'emailSend',
|
||||
'slack',
|
||||
'discord',
|
||||
'telegram',
|
||||
'postgres',
|
||||
'mysql',
|
||||
'mongodb',
|
||||
'redis',
|
||||
'github',
|
||||
'gitlab',
|
||||
'jira',
|
||||
'salesforce',
|
||||
'hubspot',
|
||||
'airtable',
|
||||
'googleSheets',
|
||||
'googleDrive',
|
||||
'dropbox',
|
||||
's3',
|
||||
'ftp',
|
||||
'ssh',
|
||||
'mqtt',
|
||||
'kafka',
|
||||
'rabbitmq',
|
||||
'graphql',
|
||||
'openai',
|
||||
'anthropic'
|
||||
];
|
||||
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.disabled) continue;
|
||||
|
||||
const normalizedType = node.type.toLowerCase();
|
||||
const isErrorProne = errorProneNodeTypes.some(type => normalizedType.includes(type));
|
||||
|
||||
// CRITICAL: Check for node-level properties in wrong location (inside parameters)
|
||||
const nodeLevelProps = [
|
||||
// Error handling properties
|
||||
'onError', 'continueOnFail', 'retryOnFail', 'maxTries', 'waitBetweenTries', 'alwaysOutputData',
|
||||
// Other node-level properties
|
||||
'executeOnce', 'disabled', 'notes', 'notesInFlow', 'credentials'
|
||||
];
|
||||
const misplacedProps: string[] = [];
|
||||
|
||||
if (node.parameters) {
|
||||
for (const prop of nodeLevelProps) {
|
||||
if (node.parameters[prop] !== undefined) {
|
||||
misplacedProps.push(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (misplacedProps.length > 0) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: `Node-level properties ${misplacedProps.join(', ')} are in the wrong location. They must be at the node level, not inside parameters.`,
|
||||
details: {
|
||||
fix: `Move these properties from node.parameters to the node level. Example:\n` +
|
||||
`{\n` +
|
||||
` "name": "${node.name}",\n` +
|
||||
` "type": "${node.type}",\n` +
|
||||
` "parameters": { /* operation-specific params */ },\n` +
|
||||
` "onError": "continueErrorOutput", // ✅ Correct location\n` +
|
||||
` "retryOnFail": true, // ✅ Correct location\n` +
|
||||
` "executeOnce": true, // ✅ Correct location\n` +
|
||||
` "disabled": false, // ✅ Correct location\n` +
|
||||
` "credentials": { /* ... */ } // ✅ Correct location\n` +
|
||||
`}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validate error handling properties
|
||||
|
||||
// Check for onError property (the modern approach)
|
||||
if (node.onError !== undefined) {
|
||||
const validOnErrorValues = ['continueRegularOutput', 'continueErrorOutput', 'stopWorkflow'];
|
||||
if (!validOnErrorValues.includes(node.onError)) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: `Invalid onError value: "${node.onError}". Must be one of: ${validOnErrorValues.join(', ')}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for deprecated continueOnFail
|
||||
if (node.continueOnFail !== undefined) {
|
||||
if (typeof node.continueOnFail !== 'boolean') {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'continueOnFail must be a boolean value'
|
||||
});
|
||||
} else if (node.continueOnFail === true) {
|
||||
// Warn about using deprecated property
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'Using deprecated "continueOnFail: true". Use "onError: \'continueRegularOutput\'" instead for better control and UI compatibility.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for conflicting error handling properties
|
||||
if (node.continueOnFail !== undefined && node.onError !== undefined) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'Cannot use both "continueOnFail" and "onError" properties. Use only "onError" for modern workflows.'
|
||||
});
|
||||
}
|
||||
|
||||
if (node.retryOnFail !== undefined) {
|
||||
if (typeof node.retryOnFail !== 'boolean') {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'retryOnFail must be a boolean value'
|
||||
});
|
||||
}
|
||||
|
||||
// If retry is enabled, check retry configuration
|
||||
if (node.retryOnFail === true) {
|
||||
if (node.maxTries !== undefined) {
|
||||
if (typeof node.maxTries !== 'number' || node.maxTries < 1) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'maxTries must be a positive number when retryOnFail is enabled'
|
||||
});
|
||||
} else if (node.maxTries > 10) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: `maxTries is set to ${node.maxTries}. Consider if this many retries is necessary.`
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// maxTries defaults to 3 if not specified
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'retryOnFail is enabled but maxTries is not specified. Default is 3 attempts.'
|
||||
});
|
||||
}
|
||||
|
||||
if (node.waitBetweenTries !== undefined) {
|
||||
if (typeof node.waitBetweenTries !== 'number' || node.waitBetweenTries < 0) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'waitBetweenTries must be a non-negative number (milliseconds)'
|
||||
});
|
||||
} else if (node.waitBetweenTries > 300000) { // 5 minutes
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: `waitBetweenTries is set to ${node.waitBetweenTries}ms (${(node.waitBetweenTries/1000).toFixed(1)}s). This seems excessive.`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.alwaysOutputData !== undefined && typeof node.alwaysOutputData !== 'boolean') {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'alwaysOutputData must be a boolean value'
|
||||
});
|
||||
}
|
||||
|
||||
// Warnings for error-prone nodes without error handling
|
||||
const hasErrorHandling = node.onError || node.continueOnFail || node.retryOnFail;
|
||||
|
||||
if (isErrorProne && !hasErrorHandling) {
|
||||
const nodeTypeSimple = normalizedType.split('.').pop() || normalizedType;
|
||||
|
||||
// Special handling for specific node types
|
||||
if (normalizedType.includes('httprequest')) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'HTTP Request node without error handling. Consider adding "onError: \'continueRegularOutput\'" for non-critical requests or "retryOnFail: true" for transient failures.'
|
||||
});
|
||||
} else if (normalizedType.includes('webhook')) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'Webhook node without error handling. Consider adding "onError: \'continueRegularOutput\'" to prevent workflow failures from blocking webhook responses.'
|
||||
});
|
||||
} else if (errorProneNodeTypes.some(db => normalizedType.includes(db) && ['postgres', 'mysql', 'mongodb'].includes(db))) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: `Database operation without error handling. Consider adding "retryOnFail: true" for connection issues or "onError: \'continueRegularOutput\'" for non-critical queries.`
|
||||
});
|
||||
} else {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: `${nodeTypeSimple} node interacts with external services but has no error handling configured. Consider using "onError" property.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for problematic combinations
|
||||
if (node.continueOnFail && node.retryOnFail) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'Both continueOnFail and retryOnFail are enabled. The node will retry first, then continue on failure.'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate additional node-level properties
|
||||
|
||||
// Check executeOnce
|
||||
if (node.executeOnce !== undefined && typeof node.executeOnce !== 'boolean') {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'executeOnce must be a boolean value'
|
||||
});
|
||||
}
|
||||
|
||||
// Check disabled
|
||||
if (node.disabled !== undefined && typeof node.disabled !== 'boolean') {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'disabled must be a boolean value'
|
||||
});
|
||||
}
|
||||
|
||||
// Check notesInFlow
|
||||
if (node.notesInFlow !== undefined && typeof node.notesInFlow !== 'boolean') {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'notesInFlow must be a boolean value'
|
||||
});
|
||||
}
|
||||
|
||||
// Check notes
|
||||
if (node.notes !== undefined && typeof node.notes !== 'string') {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'notes must be a string value'
|
||||
});
|
||||
}
|
||||
|
||||
// Provide guidance for executeOnce
|
||||
if (node.executeOnce === true) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: node.id,
|
||||
nodeName: node.name,
|
||||
message: 'executeOnce is enabled. This node will execute only once regardless of input items.'
|
||||
});
|
||||
}
|
||||
|
||||
// Suggest alwaysOutputData for debugging
|
||||
if ((node.continueOnFail || node.retryOnFail) && !node.alwaysOutputData) {
|
||||
if (normalizedType.includes('httprequest') || normalizedType.includes('webhook')) {
|
||||
result.suggestions.push(
|
||||
`Consider enabling alwaysOutputData on "${node.name}" to capture error responses for debugging`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add general suggestions based on findings
|
||||
const nodesWithoutErrorHandling = workflow.nodes.filter(n =>
|
||||
!n.disabled && !n.onError && !n.continueOnFail && !n.retryOnFail
|
||||
).length;
|
||||
|
||||
if (nodesWithoutErrorHandling > 5 && workflow.nodes.length > 5) {
|
||||
result.suggestions.push(
|
||||
'Most nodes lack error handling. Use "onError" property for modern error handling: "continueRegularOutput" (continue on error), "continueErrorOutput" (use error output), or "stopWorkflow" (stop execution).'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for nodes using deprecated continueOnFail
|
||||
const nodesWithDeprecatedErrorHandling = workflow.nodes.filter(n =>
|
||||
!n.disabled && n.continueOnFail === true
|
||||
).length;
|
||||
|
||||
if (nodesWithDeprecatedErrorHandling > 0) {
|
||||
result.suggestions.push(
|
||||
'Replace "continueOnFail: true" with "onError: \'continueRegularOutput\'" for better UI compatibility and control.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user