fix(HTTP Request Node): Cleanup circular references in response (#6590)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Romain Dunand 2023-07-12 12:31:32 +02:00 committed by GitHub
parent aaa9ee3949
commit aecc05b787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 70 additions and 71 deletions

View file

@ -8,12 +8,11 @@ import type {
INodeTypeDescription, INodeTypeDescription,
JsonObject, JsonObject,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; import { NodeApiError, NodeOperationError, sleep, removeCircularRefs } from 'n8n-workflow';
import type { OptionsWithUri } from 'request'; import type { OptionsWithUri } from 'request';
import type { IAuthDataSanitizeKeys } from '../GenericFunctions'; import type { IAuthDataSanitizeKeys } from '../GenericFunctions';
import { replaceNullValues, sanitizeUiMessage } from '../GenericFunctions'; import { replaceNullValues, sanitizeUiMessage } from '../GenericFunctions';
interface OptionData { interface OptionData {
name: string; name: string;
displayName: string; displayName: string;
@ -976,6 +975,7 @@ export class HttpRequestV1 implements INodeType {
// throw error; // throw error;
throw new NodeApiError(this.getNode(), response as JsonObject, { itemIndex }); throw new NodeApiError(this.getNode(), response as JsonObject, { itemIndex });
} else { } else {
removeCircularRefs(response.reason as JsonObject);
// Return the actual reason as error // Return the actual reason as error
returnItems.push({ returnItems.push({
json: { json: {

View file

@ -7,7 +7,7 @@ import type {
INodeTypeDescription, INodeTypeDescription,
JsonObject, JsonObject,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; import { NodeApiError, NodeOperationError, sleep, removeCircularRefs } from 'n8n-workflow';
import type { OptionsWithUri } from 'request'; import type { OptionsWithUri } from 'request';
import type { IAuthDataSanitizeKeys } from '../GenericFunctions'; import type { IAuthDataSanitizeKeys } from '../GenericFunctions';
@ -1022,12 +1022,12 @@ export class HttpRequestV2 implements INodeType {
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
// @ts-ignore // @ts-ignore
response = promisesResponses.shift(); response = promisesResponses.shift();
if (response!.status !== 'fulfilled') { if (response!.status !== 'fulfilled') {
if (!this.continueOnFail()) { if (!this.continueOnFail()) {
// throw error; // throw error;
throw new NodeApiError(this.getNode(), response as JsonObject, { itemIndex }); throw new NodeApiError(this.getNode(), response as JsonObject, { itemIndex });
} else { } else {
removeCircularRefs(response.reason as JsonObject);
// Return the actual reason as error // Return the actual reason as error
returnItems.push({ returnItems.push({
json: { json: {

View file

@ -12,7 +12,16 @@ import type {
JsonObject, JsonObject,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { BINARY_ENCODING, jsonParse, NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; import {
BINARY_ENCODING,
jsonParse,
NodeApiError,
NodeOperationError,
sleep,
removeCircularRefs,
} from 'n8n-workflow';
import { keysToLowercase } from '@utils/utilities';
import type { OptionsWithUri } from 'request-promise-native'; import type { OptionsWithUri } from 'request-promise-native';
@ -25,7 +34,6 @@ import {
replaceNullValues, replaceNullValues,
sanitizeUiMessage, sanitizeUiMessage,
} from '../GenericFunctions'; } from '../GenericFunctions';
import { keysToLowercase } from '@utils/utilities';
function toText<T>(data: T) { function toText<T>(data: T) {
if (typeof data === 'object' && data !== null) { if (typeof data === 'object' && data !== null) {
@ -1428,6 +1436,7 @@ export class HttpRequestV3 implements INodeType {
} }
throw new NodeApiError(this.getNode(), response as JsonObject, { itemIndex }); throw new NodeApiError(this.getNode(), response as JsonObject, { itemIndex });
} else { } else {
removeCircularRefs(response.reason as JsonObject);
// Return the actual reason as error // Return the actual reason as error
returnItems.push({ returnItems.push({
json: { json: {

View file

@ -14,15 +14,30 @@ import type {
NodeParameterValueType, NodeParameterValueType,
WorkflowExecuteMode, WorkflowExecuteMode,
} from './Interfaces'; } from './Interfaces';
import { ExpressionError } from './ExpressionError'; import { ExpressionError, ExpressionExtensionError } from './ExpressionError';
import { WorkflowDataProxy } from './WorkflowDataProxy'; import { WorkflowDataProxy } from './WorkflowDataProxy';
import type { Workflow } from './Workflow'; import type { Workflow } from './Workflow';
// eslint-disable-next-line import/no-cycle
import { extend, extendOptional } from './Extensions'; import { extend, extendOptional } from './Extensions';
import { extendedFunctions } from './Extensions/ExtendedFunctions'; import { extendedFunctions } from './Extensions/ExtendedFunctions';
import { extendSyntax } from './Extensions/ExpressionExtension'; import { extendSyntax } from './Extensions/ExpressionExtension';
import { isExpressionError, IS_FRONTEND, isSyntaxError, isTypeError } from './utils';
const IS_FRONTEND_IN_DEV_MODE =
typeof process === 'object' &&
Object.keys(process).length === 1 &&
'env' in process &&
Object.keys(process.env).length === 0;
const IS_FRONTEND = typeof process === 'undefined' || IS_FRONTEND_IN_DEV_MODE;
export const isSyntaxError = (error: unknown): error is SyntaxError =>
error instanceof SyntaxError || (error instanceof Error && error.name === 'SyntaxError');
export const isExpressionError = (error: unknown): error is ExpressionError =>
error instanceof ExpressionError || error instanceof ExpressionExtensionError;
export const isTypeError = (error: unknown): error is TypeError =>
error instanceof TypeError || (error instanceof Error && error.name === 'TypeError');
// Set it to use double curly brackets instead of single ones // Set it to use double curly brackets instead of single ones
tmpl.brackets.set('{{ }}'); tmpl.brackets.set('{{ }}');

View file

@ -7,6 +7,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
import { parseString } from 'xml2js'; import { parseString } from 'xml2js';
import { removeCircularRefs, isTraversableObject } from './utils';
import type { IDataObject, INode, IStatusCodeMessages, JsonObject } from './Interfaces'; import type { IDataObject, INode, IStatusCodeMessages, JsonObject } from './Interfaces';
type Severity = 'warning' | 'error'; type Severity = 'warning' | 'error';
@ -156,7 +157,7 @@ export abstract class NodeError extends ExecutionBaseError {
.map((jsonError) => { .map((jsonError) => {
if (typeof jsonError === 'string') return jsonError; if (typeof jsonError === 'string') return jsonError;
if (typeof jsonError === 'number') return jsonError.toString(); if (typeof jsonError === 'number') return jsonError.toString();
if (this.isTraversableObject(jsonError)) { if (isTraversableObject(jsonError)) {
return this.findProperty(jsonError, potentialKeys); return this.findProperty(jsonError, potentialKeys);
} }
return null; return null;
@ -168,7 +169,7 @@ export abstract class NodeError extends ExecutionBaseError {
} }
return resolvedErrors.join(' | '); return resolvedErrors.join(' | ');
} }
if (this.isTraversableObject(value)) { if (isTraversableObject(value)) {
const property = this.findProperty(value, potentialKeys); const property = this.findProperty(value, potentialKeys);
if (property) { if (property) {
return property; return property;
@ -180,7 +181,7 @@ export abstract class NodeError extends ExecutionBaseError {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const key of traversalKeys) { for (const key of traversalKeys) {
const value = jsonError[key]; const value = jsonError[key];
if (this.isTraversableObject(value)) { if (isTraversableObject(value)) {
const property = this.findProperty(value, potentialKeys, traversalKeys); const property = this.findProperty(value, potentialKeys, traversalKeys);
if (property) { if (property) {
return property; return property;
@ -190,47 +191,6 @@ export abstract class NodeError extends ExecutionBaseError {
return null; return null;
} }
/**
* Check if a value is an object with at least one key, i.e. it can be traversed.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isTraversableObject(value: any): value is JsonObject {
return (
value &&
typeof value === 'object' &&
!Array.isArray(value) &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
!!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)) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
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);
}
});
}
});
}
} }
interface NodeOperationErrorOptions { interface NodeOperationErrorOptions {
@ -317,7 +277,7 @@ export class NodeApiError extends NodeError {
if (error.error) { if (error.error) {
// only for request library error // only for request library error
this.removeCircularRefs(error.error as JsonObject); removeCircularRefs(error.error as JsonObject);
} }
if ((!message && (error.message || (error?.reason as IDataObject)?.message)) || description) { if ((!message && (error.message || (error?.reason as IDataObject)?.message)) || description) {

View file

@ -31,6 +31,7 @@ export {
sleep, sleep,
fileTypeFromMimeType, fileTypeFromMimeType,
assert, assert,
removeCircularRefs,
} from './utils'; } from './utils';
export { export {
isINodeProperties, isINodeProperties,

View file

@ -1,5 +1,4 @@
import { ExpressionError, ExpressionExtensionError } from './ExpressionError'; import type { BinaryFileType, JsonObject } from './Interfaces';
import type { BinaryFileType } from './Interfaces';
const readStreamClasses = new Set(['ReadStream', 'Readable', 'ReadableStream']); const readStreamClasses = new Set(['ReadStream', 'Readable', 'ReadableStream']);
@ -129,19 +128,34 @@ export function assert<T>(condition: T, msg?: string): asserts condition {
} }
} }
const IS_FRONTEND_IN_DEV_MODE = export const isTraversableObject = (value: any): value is JsonObject => {
typeof process === 'object' && return (
Object.keys(process).length === 1 && value &&
'env' in process && typeof value === 'object' &&
Object.keys(process.env).length === 0; !Array.isArray(value) &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
!!Object.keys(value).length
);
};
export const IS_FRONTEND = typeof process === 'undefined' || IS_FRONTEND_IN_DEV_MODE; export const removeCircularRefs = (obj: JsonObject, seen = new Set()) => {
seen.add(obj);
export const isSyntaxError = (error: unknown): error is SyntaxError => Object.entries(obj).forEach(([key, value]) => {
error instanceof SyntaxError || (error instanceof Error && error.name === 'SyntaxError'); if (isTraversableObject(value)) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
export const isExpressionError = (error: unknown): error is ExpressionError => seen.has(value) ? (obj[key] = { circularReference: true }) : removeCircularRefs(value, seen);
error instanceof ExpressionError || error instanceof ExpressionExtensionError; return;
}
export const isTypeError = (error: unknown): error is TypeError => if (Array.isArray(value)) {
error instanceof TypeError || (error instanceof Error && error.name === 'TypeError'); value.forEach((val, index) => {
if (seen.has(val)) {
value[index] = { circularReference: true };
return;
}
if (isTraversableObject(val)) {
removeCircularRefs(val, seen);
}
});
}
});
};