From aced4bf86f343f768ddb81485c24e69d5cf12530 Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:28:15 +0100 Subject: [PATCH] fix(Stop and Error Node): Show error message when error type is an object (#17898) --- .../nodes/StopAndError/StopAndError.node.ts | 28 +-- .../nodes/StopAndError/test/utils.test.ts | 206 ++++++++++++++++++ .../nodes-base/nodes/StopAndError/utils.ts | 45 ++++ 3 files changed, 260 insertions(+), 19 deletions(-) create mode 100644 packages/nodes-base/nodes/StopAndError/test/utils.test.ts create mode 100644 packages/nodes-base/nodes/StopAndError/utils.ts diff --git a/packages/nodes-base/nodes/StopAndError/StopAndError.node.ts b/packages/nodes-base/nodes/StopAndError/StopAndError.node.ts index c1113f1512..cd9ea6ac06 100644 --- a/packages/nodes-base/nodes/StopAndError/StopAndError.node.ts +++ b/packages/nodes-base/nodes/StopAndError/StopAndError.node.ts @@ -3,9 +3,10 @@ import type { INodeExecutionData, INodeType, INodeTypeDescription, - JsonObject, } from 'n8n-workflow'; -import { jsonParse, NodeConnectionTypes, NodeOperationError } from 'n8n-workflow'; +import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow'; + +import { createErrorFromParameters } from './utils'; const errorObjectPlaceholder = `{ "code": "404", @@ -81,24 +82,13 @@ export class StopAndError implements INodeType { async execute(this: IExecuteFunctions): Promise { const errorType = this.getNodeParameter('errorType', 0) as 'errorMessage' | 'errorObject'; - const { id: workflowId, name: workflowName } = this.getWorkflow(); + const errorParameter = + errorType === 'errorMessage' + ? (this.getNodeParameter('errorMessage', 0) as string) + : (this.getNodeParameter('errorObject', 0) as string); - let toThrow: string | JsonObject; + const { message, options } = createErrorFromParameters(errorType, errorParameter); - if (errorType === 'errorMessage') { - toThrow = this.getNodeParameter('errorMessage', 0) as string; - } else { - const json = this.getNodeParameter('errorObject', 0) as string; - - const errorObject = jsonParse(json); - - toThrow = { - name: 'User-thrown error', - message: `Workflow ID ${workflowId} "${workflowName}" has failed`, - ...errorObject, - }; - } - - throw new NodeOperationError(this.getNode(), toThrow); + throw new NodeOperationError(this.getNode(), message, options); } } diff --git a/packages/nodes-base/nodes/StopAndError/test/utils.test.ts b/packages/nodes-base/nodes/StopAndError/test/utils.test.ts new file mode 100644 index 0000000000..d79bb3a4ea --- /dev/null +++ b/packages/nodes-base/nodes/StopAndError/test/utils.test.ts @@ -0,0 +1,206 @@ +import { createErrorFromParameters } from '../utils'; + +describe('createErrorFromParameters', () => { + describe('Error Message Type', () => { + it('should return simple message for errorMessage type', () => { + const result = createErrorFromParameters('errorMessage', 'Simple error message'); + + expect(result.message).toBe('Simple error message'); + expect(result.options).toBeUndefined(); + }); + + it('should handle empty error message', () => { + const result = createErrorFromParameters('errorMessage', ''); + + expect(result.message).toBe(''); + expect(result.options).toBeUndefined(); + }); + + it('should handle whitespace-only error message', () => { + const result = createErrorFromParameters('errorMessage', ' '); + + expect(result.message).toBe(' '); + expect(result.options).toBeUndefined(); + }); + }); + + describe('Error Object Type', () => { + it('should extract message from error object', () => { + const errorObject = JSON.stringify({ + message: 'Custom error message', + code: '404', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Custom error message'); + expect(result.options?.level).toBe('error'); + expect(result.options?.metadata).toEqual({ + message: 'Custom error message', + code: '404', + }); + }); + + it('should use description when message is not available', () => { + const errorObject = JSON.stringify({ + description: 'Detailed error description', + code: '500', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Detailed error description'); + expect(result.options?.description).toBe('Detailed error description'); + }); + + it('should use error property when message and description are not available', () => { + const errorObject = JSON.stringify({ + error: 'Something went wrong', + code: '400', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Something went wrong'); + expect(result.options?.description).toBeUndefined(); + }); + + it('should stringify object when no recognizable message properties exist', () => { + const errorObjectData = { + code: '404', + status: 'not found', + }; + const errorObject = JSON.stringify(errorObjectData); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe(`Error: ${JSON.stringify(errorObjectData)}`); + expect(result.options?.metadata).toEqual(errorObjectData); + }); + + it('should include type and description in options when available', () => { + const errorObject = JSON.stringify({ + message: 'Main error', + description: 'Detailed description', + type: 'ValidationError', + code: '400', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Main error'); + expect(result.options?.description).toBe('Detailed description'); + expect(result.options?.type).toBe('ValidationError'); + expect(result.options?.level).toBe('error'); + }); + + it('should handle complex nested object', () => { + const errorObjectData = { + message: 'Database error', + details: { + table: 'users', + operation: 'SELECT', + constraint: 'foreign_key', + }, + code: 'DB_001', + }; + const errorObject = JSON.stringify(errorObjectData); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Database error'); + expect(result.options?.metadata).toEqual(errorObjectData); + }); + + it('should handle object with null and undefined values', () => { + const errorObject = JSON.stringify({ + message: 'Test message', + description: null, + type: undefined, + code: '200', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Test message'); + expect(result.options?.description).toBeUndefined(); + expect(result.options?.type).toBeUndefined(); + }); + + it('should prioritize message over description and error', () => { + const errorObject = JSON.stringify({ + message: 'Primary message', + description: 'Secondary description', + error: 'Tertiary error', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Primary message'); + }); + + it('should prioritize description over error when message is not available', () => { + const errorObject = JSON.stringify({ + description: 'Primary description', + error: 'Secondary error', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Primary description'); + expect(result.options?.description).toBe('Primary description'); + }); + }); + + describe('Invalid JSON Handling', () => { + it('should throw error for invalid JSON in errorObject type', () => { + expect(() => { + createErrorFromParameters('errorObject', '{invalid json}'); + }).toThrow(); + }); + + it('should handle empty JSON object', () => { + const result = createErrorFromParameters('errorObject', '{}'); + + expect(result.message).toBe('Error: {}'); + expect(result.options?.metadata).toEqual({}); + }); + }); + + describe('Edge Cases', () => { + it('should handle arrays in error object', () => { + const errorObjectData = { + message: 'Validation failed', + errors: ['Field required', 'Invalid format'], + }; + const errorObject = JSON.stringify(errorObjectData); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Validation failed'); + expect(result.options?.metadata).toEqual(errorObjectData); + }); + + it('should handle empty string values in message fields', () => { + const errorObject = JSON.stringify({ + message: '', + description: 'Fallback description', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe('Fallback description'); + }); + + it('should handle whitespace-only string values in message fields', () => { + const errorObject = JSON.stringify({ + message: ' ', + description: 'Fallback description', + }); + + const result = createErrorFromParameters('errorObject', errorObject); + + expect(result.message).toBe(' '); + }); + }); +}); diff --git a/packages/nodes-base/nodes/StopAndError/utils.ts b/packages/nodes-base/nodes/StopAndError/utils.ts new file mode 100644 index 0000000000..825a5eadb6 --- /dev/null +++ b/packages/nodes-base/nodes/StopAndError/utils.ts @@ -0,0 +1,45 @@ +import type { JsonObject } from 'n8n-workflow'; +import { jsonParse } from 'n8n-workflow'; + +export interface ErrorHandlerResult { + message: string; + options?: { + description?: string; + type?: string; + level: 'error'; + metadata?: JsonObject; + }; +} + +function isString(value: unknown): value is string { + return typeof value === 'string' && value.length > 0; +} + +export function createErrorFromParameters( + errorType: 'errorMessage' | 'errorObject', + errorParameter: string, +): ErrorHandlerResult { + if (errorType === 'errorMessage') { + return { + message: errorParameter, + }; + } else { + const errorObject = jsonParse(errorParameter); + + const errorMessage = + (isString(errorObject.message) ? errorObject.message : '') || + (isString(errorObject.description) ? errorObject.description : '') || + (isString(errorObject.error) ? errorObject.error : '') || + `Error: ${JSON.stringify(errorObject)}`; + + return { + message: errorMessage, + options: { + description: isString(errorObject.description) ? errorObject.description : undefined, + type: isString(errorObject.type) ? errorObject.type : undefined, + level: 'error', + metadata: errorObject, + }, + }; + } +}