fix: Make sure errors are transferred correctly from js task runner (no-changelog) (#11214)

This commit is contained in:
Tomi Turtiainen
2024-10-10 21:01:38 +03:00
committed by GitHub
parent 4e78c46a74
commit 1078fa662a
16 changed files with 311 additions and 122 deletions

View File

@@ -1,16 +1,13 @@
import {
ensureError,
ApplicationError,
type CodeExecutionMode,
type IExecuteFunctions,
type INodeExecutionData,
type WorkflowExecuteMode,
} from 'n8n-workflow';
import { ExecutionError } from './ExecutionError';
import {
mapItemsNotDefinedErrorIfNeededForRunForAll,
validateNoDisallowedMethodsInRunForEach,
} from './JsCodeValidator';
import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError';
import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator';
/**
* JS Code execution sandbox that executes the JS code using task runner.
@@ -23,70 +20,54 @@ export class JsTaskRunnerSandbox {
private readonly executeFunctions: IExecuteFunctions,
) {}
async runCode<T = unknown>(): Promise<T> {
const itemIndex = 0;
try {
const executionResult = (await this.executeFunctions.startJob<T>(
'javascript',
{
code: this.jsCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
},
itemIndex,
)) as T;
return executionResult;
} catch (e) {
const error = ensureError(e);
throw new ExecutionError(error);
}
}
async runCodeAllItems(): Promise<INodeExecutionData[]> {
const itemIndex = 0;
return await this.executeFunctions
.startJob<INodeExecutionData[]>(
'javascript',
{
code: this.jsCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
continueOnFail: this.executeFunctions.continueOnFail(),
},
itemIndex,
)
.catch((e) => {
const error = ensureError(e);
// anticipate user expecting `items` to pre-exist as in Function Item node
mapItemsNotDefinedErrorIfNeededForRunForAll(this.jsCode, error);
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
'javascript',
{
code: this.jsCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
continueOnFail: this.executeFunctions.continueOnFail(),
},
itemIndex,
);
throw new ExecutionError(error);
});
return executionResult.ok
? executionResult.result
: this.throwExecutionError(executionResult.error);
}
async runCodeForEachItem(): Promise<INodeExecutionData[]> {
validateNoDisallowedMethodsInRunForEach(this.jsCode, 0);
const itemIndex = 0;
return await this.executeFunctions
.startJob<INodeExecutionData[]>(
'javascript',
{
code: this.jsCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
continueOnFail: this.executeFunctions.continueOnFail(),
},
itemIndex,
)
.catch((e) => {
const error = ensureError(e);
// anticipate user expecting `items` to pre-exist as in Function Item node
mapItemsNotDefinedErrorIfNeededForRunForAll(this.jsCode, error);
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
'javascript',
{
code: this.jsCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
continueOnFail: this.executeFunctions.continueOnFail(),
},
itemIndex,
);
throw new ExecutionError(error);
});
return executionResult.ok
? executionResult.result
: this.throwExecutionError(executionResult.error);
}
private throwExecutionError(error: unknown): never {
// The error coming from task runner is not an instance of error,
// so we need to wrap it in an error instance.
if (isWrappableError(error)) {
throw new WrappedExecutionError(error);
}
throw new ApplicationError('Unknown error', {
cause: error,
});
}
}

View File

@@ -0,0 +1,35 @@
import { ApplicationError } from 'n8n-workflow';
export type WrappableError = Record<string, unknown>;
/**
* Errors received from the task runner are not instances of Error.
* This class wraps them in an Error instance and makes all their
* properties available.
*/
export class WrappedExecutionError extends ApplicationError {
[key: string]: unknown;
constructor(error: WrappableError) {
const message = typeof error.message === 'string' ? error.message : 'Unknown error';
super(message, {
cause: error,
});
this.copyErrorProperties(error);
}
private copyErrorProperties(error: WrappableError) {
for (const key of Object.getOwnPropertyNames(error)) {
if (key === 'message' || key === 'stack') {
continue;
}
this[key] = error[key];
}
}
}
export function isWrappableError(error: unknown): error is WrappableError {
return typeof error === 'object' && error !== null;
}