From e469cfb1b75f18e022514cc3f520496d12de1dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 11 Feb 2025 15:07:51 +0100 Subject: [PATCH] additional tests --- .../utils/__tests__/normalize-items.test.ts | 40 +- .../request-helper-functions.test.ts | 469 ++++++++++++++++++ .../utils/request-helper-functions.ts | 384 +++++++------- 3 files changed, 696 insertions(+), 197 deletions(-) diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/normalize-items.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/normalize-items.test.ts index a55c0325f4291..2664e1bf08e87 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/normalize-items.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/normalize-items.test.ts @@ -1,4 +1,4 @@ -import type { INodeExecutionData } from 'n8n-workflow'; +import type { IBinaryData, INodeExecutionData } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow'; import { normalizeItems } from '../normalize-items'; @@ -41,6 +41,30 @@ describe('normalizeItems', () => { { json: {}, binary: { data: { data: 'binary2', mimeType: 'mime2' } } }, ], }, + { + description: 'object with null or undefined values', + input: { key: null, another: undefined } as unknown as INodeExecutionData, + expected: [{ json: { key: null, another: undefined } }], + }, + { + description: 'array with mixed non-standard objects', + input: [{ custom: 'value1' }, { another: 'value2' }] as unknown as INodeExecutionData[], + expected: [{ json: { custom: 'value1' } }, { json: { another: 'value2' } }], + }, + { + description: 'empty object', + input: {} as INodeExecutionData, + expected: [{ json: {} }], + }, + { + description: 'array with primitive values', + input: [1, 'string', true] as unknown as INodeExecutionData[], + expected: [ + { json: 1 }, + { json: 'string' }, + { json: true }, + ] as unknown as INodeExecutionData[], + }, ]; test.each(successTests)('$description', ({ input, expected }) => { const result = normalizeItems(input); @@ -64,6 +88,20 @@ describe('normalizeItems', () => { { key: 'value' } as unknown as INodeExecutionData, ], }, + { + description: 'when mixing json and non-json objects with non-json properties', + input: [ + { json: { key1: 'value1' } }, + { other: 'value', custom: 'prop' } as unknown as INodeExecutionData, + ], + }, + { + description: 'when mixing binary and non-binary objects', + input: [ + { json: {}, binary: { data: { data: 'binarydata' } as IBinaryData } }, + { custom: 'value' } as unknown as INodeExecutionData, + ], + }, ]; test.each(errorTests)('$description', ({ input }) => { expect(() => normalizeItems(input)).toThrow(new ApplicationError('Inconsistent item format')); diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts index 0620bf0bc4800..a8dcaf08aac6c 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/request-helper-functions.test.ts @@ -7,6 +7,7 @@ import type { INode, IRequestOptions, IWorkflowExecuteAdditionalData, + PaginationOptions, Workflow, } from 'n8n-workflow'; import nock from 'nock'; @@ -15,6 +16,10 @@ import type { SecureContextOptions } from 'tls'; import type { ExecutionLifecycleHooks } from '@/execution-engine/execution-lifecycle-hooks'; import { + applyPaginationRequestData, + convertN8nRequestToAxios, + createFormDataObject, + httpRequest, invokeAxios, parseRequestObject, proxyRequestToAxios, @@ -393,4 +398,468 @@ describe('Request Helper Functions', () => { ); }); }); + + describe('createFormDataObject', () => { + test('should create FormData with simple key-value pairs', () => { + const data = { key1: 'value1', key2: 'value2' }; + const formData = createFormDataObject(data); + + expect(formData).toBeInstanceOf(FormData); + + const formDataEntries: string[] = []; + formData.getHeaders(); // Ensures form data is processed + + formData.on('data', (chunk) => { + formDataEntries.push(chunk.toString()); + }); + }); + + test('should handle array values', () => { + const data = { files: ['file1.txt', 'file2.txt'] }; + const formData = createFormDataObject(data); + + expect(formData).toBeInstanceOf(FormData); + }); + + test('should handle complex form data with options', () => { + const data = { + file: { + value: Buffer.from('test content'), + options: { + filename: 'test.txt', + contentType: 'text/plain', + }, + }, + }; + + const formData = createFormDataObject(data); + + expect(formData).toBeInstanceOf(FormData); + }); + }); + + describe('convertN8nRequestToAxios', () => { + test('should convert basic HTTP request options', () => { + const requestOptions: IHttpRequestOptions = { + method: 'GET', + url: 'https://example.com', + headers: { 'Custom-Header': 'test' }, + qs: { param1: 'value1' }, + }; + + const axiosConfig = convertN8nRequestToAxios(requestOptions); + + expect(axiosConfig).toEqual( + expect.objectContaining({ + method: 'GET', + url: 'https://example.com', + headers: expect.objectContaining({ + 'Custom-Header': 'test', + 'User-Agent': 'n8n', + }), + params: { param1: 'value1' }, + }), + ); + }); + + test('should handle body and content type', () => { + const requestOptions: IHttpRequestOptions = { + method: 'POST', + url: 'https://example.com', + body: { key: 'value' }, + headers: { 'content-type': 'application/json' }, + }; + + const axiosConfig = convertN8nRequestToAxios(requestOptions); + + expect(axiosConfig).toEqual( + expect.objectContaining({ + method: 'POST', + data: { key: 'value' }, + headers: expect.objectContaining({ + 'content-type': 'application/json', + }), + }), + ); + }); + + test('should handle form data', () => { + const formData = new FormData(); + formData.append('key', 'value'); + + const requestOptions: IHttpRequestOptions = { + method: 'POST', + url: 'https://example.com', + body: formData, + }; + + const axiosConfig = convertN8nRequestToAxios(requestOptions); + + expect(axiosConfig).toEqual( + expect.objectContaining({ + method: 'POST', + data: formData, + headers: expect.objectContaining({ + ...formData.getHeaders(), + 'User-Agent': 'n8n', + }), + }), + ); + }); + + test('should handle disable follow redirect', () => { + const requestOptions: IHttpRequestOptions = { + method: 'GET', + url: 'https://example.com', + disableFollowRedirect: true, + }; + + const axiosConfig = convertN8nRequestToAxios(requestOptions); + + expect(axiosConfig.maxRedirects).toBe(0); + }); + + test('should handle SSL certificate validation', () => { + const requestOptions: IHttpRequestOptions = { + method: 'GET', + url: 'https://example.com', + skipSslCertificateValidation: true, + }; + + const axiosConfig = convertN8nRequestToAxios(requestOptions); + + expect(axiosConfig.httpsAgent?.options.rejectUnauthorized).toBe(false); + }); + }); + + describe('applyPaginationRequestData', () => { + test('should merge pagination request data with original request options', () => { + const originalRequestOptions: IRequestOptions = { + uri: 'https://original.com/api', + method: 'GET', + qs: { page: 1 }, + headers: { 'X-Original-Header': 'original' }, + }; + + const paginationRequestData: PaginationOptions['request'] = { + url: 'https://pagination.com/api', + body: { key: 'value' }, + headers: { 'X-Pagination-Header': 'pagination' }, + }; + + const result = applyPaginationRequestData(originalRequestOptions, paginationRequestData); + + expect(result).toEqual({ + uri: 'https://pagination.com/api', + url: 'https://pagination.com/api', + method: 'GET', + qs: { page: 1 }, + headers: { + 'X-Original-Header': 'original', + 'X-Pagination-Header': 'pagination', + }, + body: { key: 'value' }, + }); + }); + + test('should handle formData correctly', () => { + const originalRequestOptions: IRequestOptions = { + uri: 'https://original.com/api', + method: 'POST', + formData: { original: 'data' }, + }; + + const paginationRequestData: PaginationOptions['request'] = { + url: 'https://pagination.com/api', + body: { key: 'value' }, + }; + + const result = applyPaginationRequestData(originalRequestOptions, paginationRequestData); + + expect(result).toEqual({ + uri: 'https://pagination.com/api', + url: 'https://pagination.com/api', + method: 'POST', + formData: { key: 'value', original: 'data' }, + }); + }); + + test('should handle form data correctly', () => { + const originalRequestOptions: IRequestOptions = { + uri: 'https://original.com/api', + method: 'POST', + form: { original: 'data' }, + }; + + const paginationRequestData: PaginationOptions['request'] = { + url: 'https://pagination.com/api', + body: { key: 'value' }, + }; + + const result = applyPaginationRequestData(originalRequestOptions, paginationRequestData); + + expect(result).toEqual({ + uri: 'https://pagination.com/api', + url: 'https://pagination.com/api', + method: 'POST', + form: { key: 'value', original: 'data' }, + }); + }); + + test('should prefer pagination body over original body', () => { + const originalRequestOptions: IRequestOptions = { + uri: 'https://original.com/api', + method: 'POST', + body: { original: 'data' }, + }; + + const paginationRequestData: PaginationOptions['request'] = { + url: 'https://pagination.com/api', + body: { key: 'value' }, + }; + + const result = applyPaginationRequestData(originalRequestOptions, paginationRequestData); + + expect(result).toEqual({ + uri: 'https://pagination.com/api', + url: 'https://pagination.com/api', + method: 'POST', + body: { key: 'value', original: 'data' }, + }); + }); + + test('should merge complex request options', () => { + const originalRequestOptions: IRequestOptions = { + uri: 'https://original.com/api', + method: 'GET', + qs: { page: 1, limit: 10 }, + headers: { 'X-Original-Header': 'original' }, + body: { filter: 'active' }, + }; + + const paginationRequestData: PaginationOptions['request'] = { + url: 'https://pagination.com/api', + body: { key: 'value' }, + headers: { 'X-Pagination-Header': 'pagination' }, + qs: { offset: 20 }, + }; + + const result = applyPaginationRequestData(originalRequestOptions, paginationRequestData); + + expect(result).toEqual({ + uri: 'https://pagination.com/api', + url: 'https://pagination.com/api', + method: 'GET', + qs: { offset: 20, limit: 10, page: 1 }, + headers: { + 'X-Original-Header': 'original', + 'X-Pagination-Header': 'pagination', + }, + body: { key: 'value', filter: 'active' }, + }); + }); + + test('should handle edge cases with empty pagination data', () => { + const originalRequestOptions: IRequestOptions = { + uri: 'https://original.com/api', + method: 'GET', + }; + + const paginationRequestData: PaginationOptions['request'] = {}; + + const result = applyPaginationRequestData(originalRequestOptions, paginationRequestData); + + expect(result).toEqual({ + uri: 'https://original.com/api', + method: 'GET', + }); + }); + }); + + describe('httpRequest', () => { + const baseUrl = 'https://example.com'; + + beforeEach(() => { + nock.cleanAll(); + }); + + test('should make a simple GET request', async () => { + const scope = nock(baseUrl) + .get('/users') + .reply(200, { users: ['John', 'Jane'] }); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/users`, + }); + + expect(response).toEqual({ users: ['John', 'Jane'] }); + scope.done(); + }); + + test('should make a POST request with JSON body', async () => { + const requestBody = { name: 'John', age: 30 }; + const scope = nock(baseUrl) + .post('/users', requestBody) + .reply(201, { id: '123', ...requestBody }); + + const response = await httpRequest({ + method: 'POST', + url: `${baseUrl}/users`, + body: requestBody, + json: true, + }); + + expect(response).toEqual({ id: '123', name: 'John', age: 30 }); + scope.done(); + }); + + test('should return full response when returnFullResponse is true', async () => { + const scope = nock(baseUrl).get('/data').reply( + 200, + { key: 'value' }, + { + 'X-Custom-Header': 'test-header', + }, + ); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/data`, + returnFullResponse: true, + }); + + expect(response).toEqual({ + body: { key: 'value' }, + headers: expect.objectContaining({ + 'x-custom-header': 'test-header', + }), + statusCode: 200, + statusMessage: 'OK', + }); + scope.done(); + }); + + test('should handle form data request', async () => { + const formData = new FormData(); + formData.append('file', 'test content', 'file.txt'); + + const scope = nock(baseUrl) + .post('/upload') + .matchHeader('content-type', /multipart\/form-data; boundary=/) + .reply(200, { success: true }); + + const response = await httpRequest({ + method: 'POST', + url: `${baseUrl}/upload`, + body: formData, + }); + + expect(response).toEqual({ success: true }); + scope.done(); + }); + + test('should handle query parameters', async () => { + const scope = nock(baseUrl) + .get('/search') + .query({ q: 'test', page: '1' }) + .reply(200, { results: ['result1', 'result2'] }); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/search`, + qs: { q: 'test', page: '1' }, + }); + + expect(response).toEqual({ results: ['result1', 'result2'] }); + scope.done(); + }); + + test('should ignore HTTP status errors when configured', async () => { + const scope = nock(baseUrl).get('/error').reply(500, { error: 'Internal Server Error' }); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/error`, + ignoreHttpStatusErrors: true, + }); + + expect(response).toEqual({ error: 'Internal Server Error' }); + scope.done(); + }); + + test('should handle different array formats in query parameters', async () => { + const scope = nock(baseUrl) + .get('/list') + .query({ + tags: ['tag1', 'tag2'], + categories: ['cat1', 'cat2'], + }) + .reply(200, { success: true }); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/list`, + qs: { + tags: ['tag1', 'tag2'], + categories: ['cat1', 'cat2'], + }, + arrayFormat: 'indices', + }); + + expect(response).toEqual({ success: true }); + scope.done(); + }); + + test('should remove empty body for GET requests', async () => { + const scope = nock(baseUrl).get('/data').reply(200, { success: true }); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/data`, + body: {}, + }); + + expect(response).toEqual({ success: true }); + scope.done(); + }); + + test('should set default user agent', async () => { + const scope = nock(baseUrl, { + reqheaders: { + 'user-agent': 'n8n', + }, + }) + .get('/test') + .reply(200, { success: true }); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/test`, + }); + + expect(response).toEqual({ success: true }); + scope.done(); + }); + + test('should respect custom headers', async () => { + const scope = nock(baseUrl, { + reqheaders: { + 'X-Custom-Header': 'custom-value', + 'user-agent': 'n8n', + }, + }) + .get('/test') + .reply(200, { success: true }); + + const response = await httpRequest({ + method: 'GET', + url: `${baseUrl}/test`, + headers: { 'X-Custom-Header': 'custom-value' }, + }); + + expect(response).toEqual({ success: true }); + scope.done(); + }); + }); }); diff --git a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts index c05e46125217c..04657029a9cb5 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts @@ -229,7 +229,7 @@ const pushFormDataValue = (form: FormData, key: string, value: any) => { } }; -const createFormDataObject = (data: Record) => { +export const createFormDataObject = (data: Record) => { const formData = new FormData(); const keys = Object.keys(data); keys.forEach((key) => { @@ -721,7 +721,7 @@ export async function proxyRequestToAxios( } // eslint-disable-next-line complexity -function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequestConfig { +export function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequestConfig { // Destructure properties with the same name first. const { headers, method, timeout, auth, proxy, url } = n8nRequest; @@ -887,12 +887,7 @@ export function applyPaginationRequestData( return merge({}, requestData, preparedPaginationData); } -/** - * Makes a request using OAuth data for authentication - * - * @param {(IHttpRequestOptions | IRequestOptions)} requestOptions - * - */ +/** @deprecated make these requests using httpRequestWithAuthentication */ export async function requestOAuth2( this: IAllExecuteFunctions, credentialsType: string, @@ -1132,9 +1127,7 @@ export async function requestOAuth2( }); } -/** - * Makes a request using OAuth1 data for authentication - */ +/** @deprecated make these requests using httpRequestWithAuthentication */ export async function requestOAuth1( this: IAllExecuteFunctions, credentialsType: string, @@ -1322,7 +1315,7 @@ export async function httpRequestWithAuthentication( } } -// TODO: Move up later +/** @deprecated use httpRequestWithAuthentication */ export async function requestWithAuthentication( this: IAllExecuteFunctions, credentialsType: string, @@ -1468,223 +1461,222 @@ export const getRequestHelperFunctions = ( return parameterValue; }; - return { - httpRequest, - // eslint-disable-next-line complexity - async requestWithAuthenticationPaginated( - this: IExecuteFunctions, - requestOptions: IRequestOptions, - itemIndex: number, - paginationOptions: PaginationOptions, - credentialsType?: string, - additionalCredentialOptions?: IAdditionalCredentialOptions, - ): Promise { - const responseData = []; - if (!requestOptions.qs) { - requestOptions.qs = {}; - } - requestOptions.resolveWithFullResponse = true; - requestOptions.simple = false; + // eslint-disable-next-line complexity + async function requestWithAuthenticationPaginated( + this: IExecuteFunctions, + requestOptions: IRequestOptions, + itemIndex: number, + paginationOptions: PaginationOptions, + credentialsType?: string, + additionalCredentialOptions?: IAdditionalCredentialOptions, + ): Promise { + const responseData = []; + if (!requestOptions.qs) { + requestOptions.qs = {}; + } + requestOptions.resolveWithFullResponse = true; + requestOptions.simple = false; - let tempResponseData: IN8nHttpFullResponse; - let makeAdditionalRequest: boolean; - let paginateRequestData: PaginationOptions['request']; + let tempResponseData: IN8nHttpFullResponse; + let makeAdditionalRequest: boolean; + let paginateRequestData: PaginationOptions['request']; - const runIndex = 0; + const runIndex = 0; - const additionalKeys: IWorkflowDataProxyAdditionalKeys = { - $request: requestOptions, - $response: {} as IN8nHttpFullResponse, - $version: node.typeVersion, - $pageCount: 0, - }; - - const executeData: IExecuteData = { - data: {}, - node, - source: null, - }; + const additionalKeys: IWorkflowDataProxyAdditionalKeys = { + $request: requestOptions, + $response: {} as IN8nHttpFullResponse, + $version: node.typeVersion, + $pageCount: 0, + }; - const hashData = { - identicalCount: 0, - previousLength: 0, - previousHash: '', - }; - do { - paginateRequestData = getResolvedValue( - paginationOptions.request as unknown as NodeParameterValueType, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as object as PaginationOptions['request']; + const executeData: IExecuteData = { + data: {}, + node, + source: null, + }; - const tempRequestOptions = applyPaginationRequestData(requestOptions, paginateRequestData); + const hashData = { + identicalCount: 0, + previousLength: 0, + previousHash: '', + }; + do { + paginateRequestData = getResolvedValue( + paginationOptions.request as unknown as NodeParameterValueType, + itemIndex, + runIndex, + executeData, + additionalKeys, + false, + ) as object as PaginationOptions['request']; - if (!validateUrl(tempRequestOptions.uri as string)) { - throw new NodeOperationError(node, `'${paginateRequestData.url}' is not a valid URL.`, { - itemIndex, - runIndex, - type: 'invalid_url', - }); - } + const tempRequestOptions = applyPaginationRequestData(requestOptions, paginateRequestData); - if (credentialsType) { - tempResponseData = await this.helpers.requestWithAuthentication.call( - this, - credentialsType, - tempRequestOptions, - additionalCredentialOptions, - ); - } else { - tempResponseData = await this.helpers.request(tempRequestOptions); - } + if (!validateUrl(tempRequestOptions.uri as string)) { + throw new NodeOperationError(node, `'${paginateRequestData.url}' is not a valid URL.`, { + itemIndex, + runIndex, + type: 'invalid_url', + }); + } - const newResponse: IN8nHttpFullResponse = Object.assign( - { - body: {}, - headers: {}, - statusCode: 0, - }, - pick(tempResponseData, ['body', 'headers', 'statusCode']), + if (credentialsType) { + tempResponseData = await this.helpers.requestWithAuthentication.call( + this, + credentialsType, + tempRequestOptions, + additionalCredentialOptions, ); + } else { + tempResponseData = await this.helpers.request(tempRequestOptions); + } - let contentBody: Exclude; + const newResponse: IN8nHttpFullResponse = Object.assign( + { + body: {}, + headers: {}, + statusCode: 0, + }, + pick(tempResponseData, ['body', 'headers', 'statusCode']), + ); - if (newResponse.body instanceof Readable && paginationOptions.binaryResult !== true) { - // Keep the original string version that we can use it to hash if needed - contentBody = await binaryToString(newResponse.body as Buffer | Readable); + let contentBody: Exclude; - const responseContentType = newResponse.headers['content-type']?.toString() ?? ''; - if (responseContentType.includes('application/json')) { - newResponse.body = jsonParse(contentBody, { fallbackValue: {} }); - } else { - newResponse.body = contentBody; - } - tempResponseData.__bodyResolved = true; - tempResponseData.body = newResponse.body; + if (newResponse.body instanceof Readable && paginationOptions.binaryResult !== true) { + // Keep the original string version that we can use it to hash if needed + contentBody = await binaryToString(newResponse.body as Buffer | Readable); + + const responseContentType = newResponse.headers['content-type']?.toString() ?? ''; + if (responseContentType.includes('application/json')) { + newResponse.body = jsonParse(contentBody, { fallbackValue: {} }); } else { - contentBody = newResponse.body; + newResponse.body = contentBody; } + tempResponseData.__bodyResolved = true; + tempResponseData.body = newResponse.body; + } else { + contentBody = newResponse.body; + } - if (paginationOptions.binaryResult !== true || tempResponseData.headers.etag) { - // If the data is not binary (and so not a stream), or an etag is present, - // we check via etag or hash if identical data is received + if (paginationOptions.binaryResult !== true || tempResponseData.headers.etag) { + // If the data is not binary (and so not a stream), or an etag is present, + // we check via etag or hash if identical data is received - let contentLength = 0; - if ('content-length' in tempResponseData.headers) { - contentLength = parseInt(tempResponseData.headers['content-length'] as string) || 0; - } + let contentLength = 0; + if ('content-length' in tempResponseData.headers) { + contentLength = parseInt(tempResponseData.headers['content-length'] as string) || 0; + } - if (hashData.previousLength === contentLength) { - let hash: string; - if (tempResponseData.headers.etag) { - // If an etag is provided, we use it as "hash" - hash = tempResponseData.headers.etag as string; - } else { - // If there is no etag, we calculate a hash from the data in the body - if (typeof contentBody !== 'string') { - contentBody = JSON.stringify(contentBody); - } - hash = crypto.createHash('md5').update(contentBody).digest('base64'); + if (hashData.previousLength === contentLength) { + let hash: string; + if (tempResponseData.headers.etag) { + // If an etag is provided, we use it as "hash" + hash = tempResponseData.headers.etag as string; + } else { + // If there is no etag, we calculate a hash from the data in the body + if (typeof contentBody !== 'string') { + contentBody = JSON.stringify(contentBody); } + hash = crypto.createHash('md5').update(contentBody).digest('base64'); + } - if (hashData.previousHash === hash) { - hashData.identicalCount += 1; - if (hashData.identicalCount > 2) { - // Length was identical 5x and hash 3x - throw new NodeOperationError( - node, - 'The returned response was identical 5x, so requests got stopped', - { - itemIndex, - description: - 'Check if "Pagination Completed When" has been configured correctly.', - }, - ); - } - } else { - hashData.identicalCount = 0; + if (hashData.previousHash === hash) { + hashData.identicalCount += 1; + if (hashData.identicalCount > 2) { + // Length was identical 5x and hash 3x + throw new NodeOperationError( + node, + 'The returned response was identical 5x, so requests got stopped', + { + itemIndex, + description: + 'Check if "Pagination Completed When" has been configured correctly.', + }, + ); } - hashData.previousHash = hash; } else { hashData.identicalCount = 0; } - hashData.previousLength = contentLength; + hashData.previousHash = hash; + } else { + hashData.identicalCount = 0; } + hashData.previousLength = contentLength; + } - responseData.push(tempResponseData); + responseData.push(tempResponseData); - additionalKeys.$response = newResponse; - additionalKeys.$pageCount = (additionalKeys.$pageCount ?? 0) + 1; + additionalKeys.$response = newResponse; + additionalKeys.$pageCount = (additionalKeys.$pageCount ?? 0) + 1; - const maxRequests = getResolvedValue( - paginationOptions.maxRequests, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as number; + const maxRequests = getResolvedValue( + paginationOptions.maxRequests, + itemIndex, + runIndex, + executeData, + additionalKeys, + false, + ) as number; - if (maxRequests && additionalKeys.$pageCount >= maxRequests) { - break; - } + if (maxRequests && additionalKeys.$pageCount >= maxRequests) { + break; + } - makeAdditionalRequest = getResolvedValue( - paginationOptions.continue, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as boolean; - - if (makeAdditionalRequest) { - if (paginationOptions.requestInterval) { - const requestInterval = getResolvedValue( - paginationOptions.requestInterval, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as number; - - await sleep(requestInterval); - } - if (tempResponseData.statusCode < 200 || tempResponseData.statusCode >= 300) { - // We have it configured to let all requests pass no matter the response code - // via "requestOptions.simple = false" to not by default fail if it is for example - // configured to stop on 404 response codes. For that reason we have to throw here - // now an error manually if the response code is not a success one. - let data = tempResponseData.body; - if (data instanceof Readable && paginationOptions.binaryResult !== true) { - data = await binaryToString(data as Buffer | Readable); - } else if (typeof data === 'object') { - data = JSON.stringify(data); - } + makeAdditionalRequest = getResolvedValue( + paginationOptions.continue, + itemIndex, + runIndex, + executeData, + additionalKeys, + false, + ) as boolean; - throw Object.assign( - new Error(`${tempResponseData.statusCode} - "${data?.toString()}"`), - { - statusCode: tempResponseData.statusCode, - error: data, - isAxiosError: true, - response: { - headers: tempResponseData.headers, - status: tempResponseData.statusCode, - statusText: tempResponseData.statusMessage, - }, - }, - ); + if (makeAdditionalRequest) { + if (paginationOptions.requestInterval) { + const requestInterval = getResolvedValue( + paginationOptions.requestInterval, + itemIndex, + runIndex, + executeData, + additionalKeys, + false, + ) as number; + + await sleep(requestInterval); + } + if (tempResponseData.statusCode < 200 || tempResponseData.statusCode >= 300) { + // We have it configured to let all requests pass no matter the response code + // via "requestOptions.simple = false" to not by default fail if it is for example + // configured to stop on 404 response codes. For that reason we have to throw here + // now an error manually if the response code is not a success one. + let data = tempResponseData.body; + if (data instanceof Readable && paginationOptions.binaryResult !== true) { + data = await binaryToString(data as Buffer | Readable); + } else if (typeof data === 'object') { + data = JSON.stringify(data); } + + throw Object.assign(new Error(`${tempResponseData.statusCode} - "${data?.toString()}"`), { + statusCode: tempResponseData.statusCode, + error: data, + isAxiosError: true, + response: { + headers: tempResponseData.headers, + status: tempResponseData.statusCode, + statusText: tempResponseData.statusMessage, + }, + }); } - } while (makeAdditionalRequest); + } + } while (makeAdditionalRequest); - return responseData; - }, + return responseData; + } + + return { + httpRequest, + requestWithAuthenticationPaginated, async httpRequestWithAuthentication( this, credentialsType,