🐛 Remove circular references from error objects (#1802)

*  Add circular references removal

* 🔥 Remove unused flag from affected node

* 🔥 Remove unused exports

* 🔨 refactor removing circular references

*  Replace IRawErrorObject with JsonObject

*  Make error detection depth-first (#1800)

* 👕 fix type

* 🔨 improve readability

* 📝 improve placeholder for circular reference

*  Turn marker into object to keep description

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
This commit is contained in:
Iván Ovejero 2021-06-12 17:15:23 +02:00 committed by GitHub
parent c0ec1ed606
commit b2e0bcea16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 56 additions and 20 deletions

View file

@ -57,7 +57,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
throw new NodeApiError(this.getNode(), error, { parseXml: true }); throw new NodeApiError(this.getNode(), error);
} }
} }

View file

@ -5,6 +5,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
JsonObject,
NodeApiError, NodeApiError,
NodeOperationError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -95,7 +96,7 @@ export class Discord implements INodeType {
} while (--maxTries); } while (--maxTries);
if (maxTries <= 0) { if (maxTries <= 0) {
throw new NodeApiError(this.getNode(), { request: options }, { message: 'Could not send message. Max. amount of rate-limit retries got reached.' }); throw new NodeApiError(this.getNode(), { request: options } as JsonObject, { message: 'Could not send message. Max. amount of rate-limit retries got reached.' });
} }
returnData.push({success: true}); returnData.push({success: true});

View file

@ -7,6 +7,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
JsonObject,
NodeApiError, NodeApiError,
NodeOperationError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -331,7 +332,7 @@ export class RabbitMQ implements INodeType {
// @ts-ignore // @ts-ignore
const promisesResponses = await Promise.allSettled(queuePromises); const promisesResponses = await Promise.allSettled(queuePromises);
promisesResponses.forEach((response: IDataObject) => { promisesResponses.forEach((response: JsonObject) => {
if (response!.status !== 'fulfilled') { if (response!.status !== 'fulfilled') {
if (this.continueOnFail() !== true) { if (this.continueOnFail() !== true) {
@ -396,7 +397,7 @@ export class RabbitMQ implements INodeType {
// @ts-ignore // @ts-ignore
const promisesResponses = await Promise.allSettled(exchangePromises); const promisesResponses = await Promise.allSettled(exchangePromises);
promisesResponses.forEach((response: IDataObject) => { promisesResponses.forEach((response: JsonObject) => {
if (response!.status !== 'fulfilled') { if (response!.status !== 'fulfilled') {
if (this.continueOnFail() !== true) { if (this.continueOnFail() !== true) {

View file

@ -8,6 +8,7 @@ import {
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
IWebhookResponseData, IWebhookResponseData,
JsonObject,
NodeApiError, NodeApiError,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -200,7 +201,7 @@ export class TypeformTrigger implements INodeType {
(bodyData.form_response as IDataObject).definition === undefined || (bodyData.form_response as IDataObject).definition === undefined ||
(bodyData.form_response as IDataObject).answers === undefined (bodyData.form_response as IDataObject).answers === undefined
) { ) {
throw new NodeApiError(this.getNode(), bodyData, { message: 'Expected definition/answers data is missing!' }); throw new NodeApiError(this.getNode(), bodyData as JsonObject, { message: 'Expected definition/answers data is missing!' });
} }
const answers = (bodyData.form_response as IDataObject).answers as ITypeformAnswer[]; const answers = (bodyData.form_response as IDataObject).answers as ITypeformAnswer[];

View file

@ -776,10 +776,11 @@ export interface ILogger {
warn: (message: string, meta?: object) => void; warn: (message: string, meta?: object) => void;
error: (message: string, meta?: object) => void; error: (message: string, meta?: object) => void;
} }
export interface IRawErrorObject {
[key: string]: string | object | number | boolean | undefined | null | string[] | object[] | number[] | boolean[];
}
export interface IStatusCodeMessages { export interface IStatusCodeMessages {
[key: string]: string; [key: string]: string;
} }
export type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
export type JsonObject = { [key: string]: JsonValue };

View file

@ -1,4 +1,4 @@
import { INode, IRawErrorObject, IStatusCodeMessages} from '.'; import { INode, IStatusCodeMessages, JsonObject} from '.';
import { parseString } from 'xml2js'; import { parseString } from 'xml2js';
/** /**
@ -46,12 +46,13 @@ const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
*/ */
abstract class NodeError extends Error { abstract class NodeError extends Error {
description: string | null | undefined; description: string | null | undefined;
cause: Error | IRawErrorObject; cause: Error | JsonObject;
node: INode; node: INode;
timestamp: number; timestamp: number;
constructor(node: INode, error: Error | IRawErrorObject) { constructor(node: INode, error: Error | JsonObject) {
super(); super();
this.removeCircularRefs(error as JsonObject);
this.name = this.constructor.name; this.name = this.constructor.name;
this.cause = error; this.cause = error;
this.node = node; this.node = node;
@ -72,7 +73,9 @@ abstract class NodeError extends Error {
* (2) if an array, * (2) if an array,
* its string or number elements are collected as a long string, * its string or number elements are collected as a long string,
* its object elements are traversed recursively (restart this function * its object elements are traversed recursively (restart this function
* with each object as a starting point) * with each object as a starting point), or
* (3) if it is an object, it traverses the object and nested ones recursively
* based on the `potentialKeys` and returns a string if found.
* *
* If nothing found via `potentialKeys` this method iterates over `traversalKeys` and * If nothing found via `potentialKeys` this method iterates over `traversalKeys` and
* if the value at the key is a traversable object, it restarts with the object as the * if the value at the key is a traversable object, it restarts with the object as the
@ -83,15 +86,15 @@ abstract class NodeError extends Error {
* Otherwise, if all the paths have been exhausted and no value is eligible, `null` is * Otherwise, if all the paths have been exhausted and no value is eligible, `null` is
* returned. * returned.
* *
* @param {IRawErrorObject} error * @param {JsonObject} error
* @param {string[]} potentialKeys * @param {string[]} potentialKeys
* @param {string[]} traversalKeys * @param {string[]} traversalKeys
* @returns {string | null} * @returns {string | null}
*/ */
protected findProperty( protected findProperty(
error: IRawErrorObject, error: JsonObject,
potentialKeys: string[], potentialKeys: string[],
traversalKeys: string[], traversalKeys: string[] = [],
): string | null { ): string | null {
for(const key of potentialKeys) { for(const key of potentialKeys) {
if (error[key]) { if (error[key]) {
@ -103,7 +106,7 @@ abstract class NodeError extends Error {
if (typeof error === 'string') return error; if (typeof error === 'string') return error;
if (typeof error === 'number') return error.toString(); if (typeof error === 'number') return error.toString();
if (this.isTraversableObject(error)) { if (this.isTraversableObject(error)) {
return this.findProperty(error, potentialKeys, traversalKeys); return this.findProperty(error, potentialKeys);
} }
return null; return null;
}) })
@ -114,12 +117,18 @@ abstract class NodeError extends Error {
} }
return resolvedErrors.join(' | '); return resolvedErrors.join(' | ');
} }
if (this.isTraversableObject(error[key])) {
const property = this.findProperty(error[key] as JsonObject, potentialKeys);
if (property) {
return property;
}
}
} }
} }
for (const key of traversalKeys) { for (const key of traversalKeys) {
if (this.isTraversableObject(error[key])) { if (this.isTraversableObject(error[key])) {
const property = this.findProperty(error[key] as IRawErrorObject, potentialKeys, traversalKeys); const property = this.findProperty(error[key] as JsonObject, potentialKeys, traversalKeys);
if (property) { if (property) {
return property; return property;
} }
@ -132,9 +141,33 @@ abstract class NodeError extends Error {
/** /**
* Check if a value is an object with at least one key, i.e. it can be traversed. * Check if a value is an object with at least one key, i.e. it can be traversed.
*/ */
private isTraversableObject(value: any): value is IRawErrorObject { // tslint:disable-line:no-any protected isTraversableObject(value: any): value is JsonObject { // tslint:disable-line:no-any
return value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length; return value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length;
} }
/**
* Remove circular references from objects.
*/
protected removeCircularRefs(obj: JsonObject, seen = new Set()) {
seen.add(obj);
Object.entries(obj).forEach(([key, value]) => {
if (this.isTraversableObject(value)) {
seen.has(value) ? obj[key] = { circularReference: true } : this.removeCircularRefs(value, seen);
return;
}
if (Array.isArray(value)) {
value.forEach((val, index) => {
if (seen.has(val)) {
value[index] = { circularReference: true };
return;
}
if (this.isTraversableObject(val)) {
this.removeCircularRefs(val, seen);
}
});
}
});
}
} }
/** /**
@ -178,7 +211,7 @@ export class NodeApiError extends NodeError {
constructor( constructor(
node: INode, node: INode,
error: IRawErrorObject, error: JsonObject,
{ message, description, httpCode, parseXml }: { message?: string, description?: string, httpCode?: string, parseXml?: boolean } = {}, { message, description, httpCode, parseXml }: { message?: string, description?: string, httpCode?: string, parseXml?: boolean } = {},
) { ) {
super(node, error); super(node, error);
@ -215,7 +248,6 @@ export class NodeApiError extends NodeError {
* @returns {void} * @returns {void}
*/ */
private setMessage() { private setMessage() {
if (!this.httpCode) { if (!this.httpCode) {
this.httpCode = null; this.httpCode = null;
this.message = UNKNOWN_ERROR_MESSAGE; this.message = UNKNOWN_ERROR_MESSAGE;