import set from 'lodash/set'; import type { IBinaryKeyData, IDataObject, IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeBaseDescription, INodeTypeDescription, IRequestOptionsSimplified, PaginationOptions, JsonObject, IRequestOptions, IHttpRequestMethods, } from 'n8n-workflow'; import { BINARY_ENCODING, NodeApiError, NodeExecutionOutput, NodeConnectionType, NodeOperationError, jsonParse, removeCircularRefs, sleep, } from 'n8n-workflow'; import type { Readable } from 'stream'; import { keysToLowercase } from '@utils/utilities'; import type { BodyParameter, IAuthDataSanitizeKeys } from '../GenericFunctions'; import { binaryContentTypes, getOAuth2AdditionalParameters, getSecrets, prepareRequestBody, reduceAsync, replaceNullValues, sanitizeUiMessage, setAgentOptions, } from '../GenericFunctions'; import { mainProperties } from './Description'; function toText(data: T) { if (typeof data === 'object' && data !== null) { return JSON.stringify(data); } return data; } export class HttpRequestV3 implements INodeType { description: INodeTypeDescription; constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, subtitle: '={{$parameter["method"] + ": " + $parameter["url"]}}', version: [3, 4, 4.1, 4.2], defaults: { name: 'HTTP Request', color: '#0004F5', }, inputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main], credentials: [ { name: 'httpSslAuth', required: true, displayOptions: { show: { provideSslCertificates: [true], }, }, }, ], properties: mainProperties, }; } async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const nodeVersion = this.getNode().typeVersion; const fullResponseProperties = ['body', 'headers', 'statusCode', 'statusMessage']; let authentication; try { authentication = this.getNodeParameter('authentication', 0) as | 'predefinedCredentialType' | 'genericCredentialType' | 'none'; } catch {} let httpBasicAuth; let httpDigestAuth; let httpHeaderAuth; let httpQueryAuth; let httpCustomAuth; let oAuth1Api; let oAuth2Api; let sslCertificates; let nodeCredentialType: string | undefined; let genericCredentialType: string | undefined; let requestOptions: IRequestOptions = { uri: '', }; let returnItems: INodeExecutionData[] = []; const requestPromises = []; let fullResponse = false; let autoDetectResponseFormat = false; // Can not be defined on a per item level const pagination = this.getNodeParameter('options.pagination.pagination', 0, null, { rawExpressions: true, }) as { paginationMode: 'off' | 'updateAParameterInEachRequest' | 'responseContainsNextURL'; nextURL?: string; parameters: { parameters: Array<{ type: 'body' | 'headers' | 'qs'; name: string; value: string; }>; }; paginationCompleteWhen: 'responseIsEmpty' | 'receiveSpecificStatusCodes' | 'other'; statusCodesWhenComplete: string; completeExpression: string; limitPagesFetched: boolean; maxRequests: number; requestInterval: number; }; const requests: Array<{ options: IRequestOptions; authKeys: IAuthDataSanitizeKeys; credentialType?: string; }> = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { if (authentication === 'genericCredentialType') { genericCredentialType = this.getNodeParameter('genericAuthType', 0) as string; if (genericCredentialType === 'httpBasicAuth') { httpBasicAuth = await this.getCredentials('httpBasicAuth', itemIndex); } else if (genericCredentialType === 'httpDigestAuth') { httpDigestAuth = await this.getCredentials('httpDigestAuth', itemIndex); } else if (genericCredentialType === 'httpHeaderAuth') { httpHeaderAuth = await this.getCredentials('httpHeaderAuth', itemIndex); } else if (genericCredentialType === 'httpQueryAuth') { httpQueryAuth = await this.getCredentials('httpQueryAuth', itemIndex); } else if (genericCredentialType === 'httpCustomAuth') { httpCustomAuth = await this.getCredentials('httpCustomAuth', itemIndex); } else if (genericCredentialType === 'oAuth1Api') { oAuth1Api = await this.getCredentials('oAuth1Api', itemIndex); } else if (genericCredentialType === 'oAuth2Api') { oAuth2Api = await this.getCredentials('oAuth2Api', itemIndex); } } else if (authentication === 'predefinedCredentialType') { nodeCredentialType = this.getNodeParameter('nodeCredentialType', itemIndex) as string; } const provideSslCertificates = this.getNodeParameter( 'provideSslCertificates', itemIndex, false, ); if (provideSslCertificates) { sslCertificates = await this.getCredentials('httpSslAuth', itemIndex); } const requestMethod = this.getNodeParameter('method', itemIndex) as IHttpRequestMethods; const sendQuery = this.getNodeParameter('sendQuery', itemIndex, false) as boolean; const queryParameters = this.getNodeParameter( 'queryParameters.parameters', itemIndex, [], ) as [{ name: string; value: string }]; const specifyQuery = this.getNodeParameter('specifyQuery', itemIndex, 'keypair') as string; const jsonQueryParameter = this.getNodeParameter('jsonQuery', itemIndex, '') as string; const sendBody = this.getNodeParameter('sendBody', itemIndex, false) as boolean; const bodyContentType = this.getNodeParameter('contentType', itemIndex, '') as string; const specifyBody = this.getNodeParameter('specifyBody', itemIndex, '') as string; const bodyParameters = this.getNodeParameter( 'bodyParameters.parameters', itemIndex, [], ) as BodyParameter[]; const jsonBodyParameter = this.getNodeParameter('jsonBody', itemIndex, '') as string; const body = this.getNodeParameter('body', itemIndex, '') as string; const sendHeaders = this.getNodeParameter('sendHeaders', itemIndex, false) as boolean; const headerParameters = this.getNodeParameter( 'headerParameters.parameters', itemIndex, [], ) as [{ name: string; value: string }]; const specifyHeaders = this.getNodeParameter( 'specifyHeaders', itemIndex, 'keypair', ) as string; const jsonHeadersParameter = this.getNodeParameter('jsonHeaders', itemIndex, '') as string; const { redirect, batching, proxy, timeout, allowUnauthorizedCerts, queryParameterArrays, response, lowercaseHeaders, } = this.getNodeParameter('options', itemIndex, {}) as { batching: { batch: { batchSize: number; batchInterval: number } }; proxy: string; timeout: number; allowUnauthorizedCerts: boolean; queryParameterArrays: 'indices' | 'brackets' | 'repeat'; response: { response: { neverError: boolean; responseFormat: string; fullResponse: boolean }; }; redirect: { redirect: { maxRedirects: number; followRedirects: boolean } }; lowercaseHeaders: boolean; }; const url = this.getNodeParameter('url', itemIndex) as string; const responseFormat = response?.response?.responseFormat || 'autodetect'; fullResponse = response?.response?.fullResponse || false; autoDetectResponseFormat = responseFormat === 'autodetect'; // defaults batch size to 1 of it's set to 0 const batchSize = batching?.batch?.batchSize > 0 ? batching?.batch?.batchSize : 1; const batchInterval = batching?.batch.batchInterval; if (itemIndex > 0 && batchSize >= 0 && batchInterval > 0) { if (itemIndex % batchSize === 0) { await sleep(batchInterval); } } requestOptions = { headers: {}, method: requestMethod, uri: url, gzip: true, rejectUnauthorized: !allowUnauthorizedCerts || false, followRedirect: false, resolveWithFullResponse: true, }; if (requestOptions.method !== 'GET' && nodeVersion >= 4.1) { requestOptions = { ...requestOptions, followAllRedirects: false }; } const defaultRedirect = nodeVersion >= 4 && redirect === undefined; if (redirect?.redirect?.followRedirects || defaultRedirect) { requestOptions.followRedirect = true; requestOptions.followAllRedirects = true; } if (redirect?.redirect?.maxRedirects || defaultRedirect) { requestOptions.maxRedirects = redirect?.redirect?.maxRedirects; } if (response?.response?.neverError) { requestOptions.simple = false; } if (proxy) { requestOptions.proxy = proxy; } if (timeout) { requestOptions.timeout = timeout; } else { // set default timeout to 5 minutes requestOptions.timeout = 300_000; } if (sendQuery && queryParameterArrays) { Object.assign(requestOptions, { qsStringifyOptions: { arrayFormat: queryParameterArrays }, }); } const parametersToKeyValue = async ( accumulator: { [key: string]: any }, cur: { name: string; value: string; parameterType?: string; inputDataFieldName?: string }, ) => { if (cur.parameterType === 'formBinaryData') { if (!cur.inputDataFieldName) return accumulator; const binaryData = this.helpers.assertBinaryData(itemIndex, cur.inputDataFieldName); let uploadData: Buffer | Readable; const itemBinaryData = items[itemIndex].binary![cur.inputDataFieldName]; if (itemBinaryData.id) { uploadData = await this.helpers.getBinaryStream(itemBinaryData.id); } else { uploadData = Buffer.from(itemBinaryData.data, BINARY_ENCODING); } accumulator[cur.name] = { value: uploadData, options: { filename: binaryData.fileName, contentType: binaryData.mimeType, }, }; return accumulator; } accumulator[cur.name] = cur.value; return accumulator; }; // Get parameters defined in the UI if (sendBody && bodyParameters) { if (specifyBody === 'keypair' || bodyContentType === 'multipart-form-data') { requestOptions.body = await prepareRequestBody( bodyParameters, bodyContentType, nodeVersion, parametersToKeyValue, ); } else if (specifyBody === 'json') { // body is specified using JSON if (typeof jsonBodyParameter !== 'object' && jsonBodyParameter !== null) { try { JSON.parse(jsonBodyParameter); } catch { throw new NodeOperationError( this.getNode(), 'JSON parameter need to be an valid JSON', { itemIndex, }, ); } requestOptions.body = jsonParse(jsonBodyParameter); } else { requestOptions.body = jsonBodyParameter; } } else if (specifyBody === 'string') { //form urlencoded requestOptions.body = Object.fromEntries(new URLSearchParams(body)); } } // Change the way data get send in case a different content-type than JSON got selected if (sendBody && ['PATCH', 'POST', 'PUT', 'GET'].includes(requestMethod)) { if (bodyContentType === 'multipart-form-data') { requestOptions.formData = requestOptions.body as IDataObject; delete requestOptions.body; } else if (bodyContentType === 'form-urlencoded') { requestOptions.form = requestOptions.body as IDataObject; delete requestOptions.body; } else if (bodyContentType === 'binaryData') { const inputDataFieldName = this.getNodeParameter( 'inputDataFieldName', itemIndex, ) as string; let uploadData: Buffer | Readable; let contentLength: number; const itemBinaryData = this.helpers.assertBinaryData(itemIndex, inputDataFieldName); if (itemBinaryData.id) { uploadData = await this.helpers.getBinaryStream(itemBinaryData.id); const metadata = await this.helpers.getBinaryMetadata(itemBinaryData.id); contentLength = metadata.fileSize; } else { uploadData = Buffer.from(itemBinaryData.data, BINARY_ENCODING); contentLength = uploadData.length; } requestOptions.body = uploadData; requestOptions.headers = { ...requestOptions.headers, 'content-length': contentLength, 'content-type': itemBinaryData.mimeType ?? 'application/octet-stream', }; } else if (bodyContentType === 'raw') { requestOptions.body = body; } } // Get parameters defined in the UI if (sendQuery && queryParameters) { if (specifyQuery === 'keypair') { requestOptions.qs = await reduceAsync(queryParameters, parametersToKeyValue); } else if (specifyQuery === 'json') { // query is specified using JSON try { JSON.parse(jsonQueryParameter); } catch { throw new NodeOperationError( this.getNode(), 'JSON parameter need to be an valid JSON', { itemIndex, }, ); } requestOptions.qs = jsonParse(jsonQueryParameter); } } // Get parameters defined in the UI if (sendHeaders && headerParameters) { let additionalHeaders: IDataObject = {}; if (specifyHeaders === 'keypair') { additionalHeaders = await reduceAsync( headerParameters.filter((header) => header.name), parametersToKeyValue, ); } else if (specifyHeaders === 'json') { // body is specified using JSON try { JSON.parse(jsonHeadersParameter); } catch { throw new NodeOperationError( this.getNode(), 'JSON parameter need to be an valid JSON', { itemIndex, }, ); } additionalHeaders = jsonParse(jsonHeadersParameter); } requestOptions.headers = { ...requestOptions.headers, ...(lowercaseHeaders === undefined || lowercaseHeaders ? keysToLowercase(additionalHeaders) : additionalHeaders), }; } if (autoDetectResponseFormat || responseFormat === 'file') { requestOptions.encoding = null; requestOptions.json = false; requestOptions.useStream = true; } else if (bodyContentType === 'raw') { requestOptions.json = false; requestOptions.useStream = true; } else { requestOptions.json = true; } // Add Content Type if any are set if (bodyContentType === 'raw') { if (requestOptions.headers === undefined) { requestOptions.headers = {}; } const rawContentType = this.getNodeParameter('rawContentType', itemIndex) as string; requestOptions.headers['content-type'] = rawContentType; } const authDataKeys: IAuthDataSanitizeKeys = {}; // Add SSL certificates if any are set setAgentOptions(requestOptions, sslCertificates); if (requestOptions.agentOptions) { authDataKeys.agentOptions = Object.keys(requestOptions.agentOptions); } // Add credentials if any are set if (httpBasicAuth !== undefined) { requestOptions.auth = { user: httpBasicAuth.user as string, pass: httpBasicAuth.password as string, }; authDataKeys.auth = ['pass']; } if (httpHeaderAuth !== undefined) { requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value; authDataKeys.headers = [httpHeaderAuth.name as string]; } if (httpQueryAuth !== undefined) { if (!requestOptions.qs) { requestOptions.qs = {}; } requestOptions.qs[httpQueryAuth.name as string] = httpQueryAuth.value; authDataKeys.qs = [httpQueryAuth.name as string]; } if (httpDigestAuth !== undefined) { requestOptions.auth = { user: httpDigestAuth.user as string, pass: httpDigestAuth.password as string, sendImmediately: false, }; authDataKeys.auth = ['pass']; } if (httpCustomAuth !== undefined) { const customAuth = jsonParse( (httpCustomAuth.json as string) || '{}', { errorMessage: 'Invalid Custom Auth JSON' }, ); if (customAuth.headers) { requestOptions.headers = { ...requestOptions.headers, ...customAuth.headers }; authDataKeys.headers = Object.keys(customAuth.headers); } if (customAuth.body) { requestOptions.body = { ...(requestOptions.body as IDataObject), ...customAuth.body }; authDataKeys.body = Object.keys(customAuth.body); } if (customAuth.qs) { requestOptions.qs = { ...requestOptions.qs, ...customAuth.qs }; authDataKeys.qs = Object.keys(customAuth.qs); } } if (requestOptions.headers!.accept === undefined) { if (responseFormat === 'json') { requestOptions.headers!.accept = 'application/json,text/*;q=0.99'; } else if (responseFormat === 'text') { requestOptions.headers!.accept = 'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, */*;q=0.1'; } else { requestOptions.headers!.accept = 'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7'; } } requests.push({ options: requestOptions, authKeys: authDataKeys, credentialType: nodeCredentialType, }); if (pagination && pagination.paginationMode !== 'off') { let continueExpression = '={{false}}'; if (pagination.paginationCompleteWhen === 'receiveSpecificStatusCodes') { // Split out comma separated list of status codes into array const statusCodesWhenCompleted = pagination.statusCodesWhenComplete .split(',') .map((item) => parseInt(item.trim())); continueExpression = `={{ !${JSON.stringify( statusCodesWhenCompleted, )}.includes($response.statusCode) }}`; } else if (pagination.paginationCompleteWhen === 'responseIsEmpty') { continueExpression = '={{ Array.isArray($response.body) ? $response.body.length : !!$response.body }}'; } else { // Other if (!pagination.completeExpression.length || pagination.completeExpression[0] !== '=') { throw new NodeOperationError(this.getNode(), 'Invalid or empty Complete Expression'); } continueExpression = `={{ !(${pagination.completeExpression.trim().slice(3, -2)}) }}`; } const paginationData: PaginationOptions = { continue: continueExpression, request: {}, requestInterval: pagination.requestInterval, }; if (pagination.paginationMode === 'updateAParameterInEachRequest') { // Iterate over all parameters and add them to the request paginationData.request = {}; const { parameters } = pagination.parameters; if (parameters.length === 1 && parameters[0].name === '' && parameters[0].value === '') { throw new NodeOperationError( this.getNode(), "At least one entry with 'Name' and 'Value' filled must be included in 'Parameters' to use 'Update a Parameter in Each Request' mode ", ); } pagination.parameters.parameters.forEach((parameter, index) => { if (!paginationData.request[parameter.type]) { paginationData.request[parameter.type] = {}; } const parameterName = parameter.name; if (parameterName === '') { throw new NodeOperationError( this.getNode(), `Parameter name must be set for parameter [${index + 1}] in pagination settings`, ); } const parameterValue = parameter.value; if (parameterValue === '') { throw new NodeOperationError( this.getNode(), `Some value must be provided for parameter [${ index + 1 }] in pagination settings, omitting it will result in an infinite loop`, ); } paginationData.request[parameter.type]![parameterName] = parameterValue; }); } else if (pagination.paginationMode === 'responseContainsNextURL') { paginationData.request.url = pagination.nextURL; } if (pagination.limitPagesFetched) { paginationData.maxRequests = pagination.maxRequests; } if (responseFormat === 'file') { paginationData.binaryResult = true; } const requestPromise = this.helpers.requestWithAuthenticationPaginated .call( this, requestOptions, itemIndex, paginationData, nodeCredentialType ?? genericCredentialType, ) .catch((error) => { if (error instanceof NodeOperationError && error.type === 'invalid_url') { const urlParameterName = pagination.paginationMode === 'responseContainsNextURL' ? 'Next URL' : 'URL'; throw new NodeOperationError(this.getNode(), error.message, { description: `Make sure the "${urlParameterName}" parameter evaluates to a valid URL.`, }); } throw error; }); requestPromises.push(requestPromise); } else if (authentication === 'genericCredentialType' || authentication === 'none') { if (oAuth1Api) { const requestOAuth1 = this.helpers.requestOAuth1.call(this, 'oAuth1Api', requestOptions); requestOAuth1.catch(() => {}); requestPromises.push(requestOAuth1); } else if (oAuth2Api) { const requestOAuth2 = this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, { tokenType: 'Bearer', }); requestOAuth2.catch(() => {}); requestPromises.push(requestOAuth2); } else { // bearerAuth, queryAuth, headerAuth, digestAuth, none const request = this.helpers.request(requestOptions); request.catch(() => {}); requestPromises.push(request); } } else if (authentication === 'predefinedCredentialType' && nodeCredentialType) { const additionalOAuth2Options = getOAuth2AdditionalParameters(nodeCredentialType); // service-specific cred: OAuth1, OAuth2, plain const requestWithAuthentication = this.helpers.requestWithAuthentication.call( this, nodeCredentialType, requestOptions, additionalOAuth2Options && { oauth2: additionalOAuth2Options }, itemIndex, ); requestWithAuthentication.catch(() => {}); requestPromises.push(requestWithAuthentication); } } const sanitizedRequests: IDataObject[] = []; const promisesResponses = await Promise.allSettled( requestPromises.map( async (requestPromise, itemIndex) => await requestPromise.finally(async () => { try { // Secrets need to be read after the request because secrets could have changed // For example: OAuth token refresh, preAuthentication const { options, authKeys, credentialType } = requests[itemIndex]; let secrets: string[] = []; if (credentialType) { const properties = this.getCredentialsProperties(credentialType); const credentials = await this.getCredentials(credentialType, itemIndex); secrets = getSecrets(properties, credentials); } const sanitizedRequestOptions = sanitizeUiMessage(options, authKeys, secrets); sanitizedRequests.push(sanitizedRequestOptions); this.sendMessageToUI(sanitizedRequestOptions); } catch (e) {} }), ), ); let responseData: any; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { responseData = promisesResponses.shift(); if (responseData!.status !== 'fulfilled') { if (responseData.reason.statusCode === 429) { responseData.reason.message = "Try spacing your requests out using the batching settings under 'Options'"; } if (!this.continueOnFail()) { if (autoDetectResponseFormat && responseData.reason.error instanceof Buffer) { responseData.reason.error = Buffer.from(responseData.reason.error as Buffer).toString(); } let error; if (responseData?.reason instanceof NodeApiError) { error = responseData.reason; set(error, 'context.itemIndex', itemIndex); } else { const errorData = ( responseData.reason ? responseData.reason : responseData ) as JsonObject; error = new NodeApiError(this.getNode(), errorData, { itemIndex }); } set(error, 'context.request', sanitizedRequests[itemIndex]); throw error; } else { removeCircularRefs(responseData.reason as JsonObject); // Return the actual reason as error returnItems.push({ json: { error: responseData.reason, }, pairedItem: { item: itemIndex, }, }); continue; } } let responses: any[]; if (Array.isArray(responseData.value)) { responses = responseData.value; } else { responses = [responseData.value]; } let responseFormat = this.getNodeParameter( 'options.response.response.responseFormat', 0, 'autodetect', ) as string; fullResponse = this.getNodeParameter( 'options.response.response.fullResponse', 0, false, ) as boolean; // eslint-disable-next-line prefer-const for (let [index, response] of Object.entries(responses)) { if (response?.request?.constructor.name === 'ClientRequest') delete response.request; if (this.getMode() === 'manual' && index === '0') { // For manual executions save the first response in the context // so that we can use it in the frontend and so make it easier for // the users to create the required pagination expressions const nodeContext = this.getContext('node'); if (pagination && pagination.paginationMode !== 'off') { nodeContext.response = responseData.value[0]; } else { nodeContext.response = responseData.value; } } const responseContentType = response.headers['content-type'] ?? ''; if (autoDetectResponseFormat) { if (responseContentType.includes('application/json')) { responseFormat = 'json'; if (!response.__bodyResolved) { const neverError = this.getNodeParameter( 'options.response.response.neverError', 0, false, ) as boolean; const data = await this.helpers.binaryToString(response.body as Buffer | Readable); response.body = jsonParse(data, { ...(neverError ? { fallbackValue: {} } : { errorMessage: 'Invalid JSON in response body' }), }); } } else if (binaryContentTypes.some((e) => responseContentType.includes(e))) { responseFormat = 'file'; } else { responseFormat = 'text'; if (!response.__bodyResolved) { const data = await this.helpers.binaryToString(response.body as Buffer | Readable); response.body = !data ? undefined : data; } } } if (autoDetectResponseFormat && !fullResponse) { delete response.headers; delete response.statusCode; delete response.statusMessage; } if (!fullResponse) { response = response.body; } if (responseFormat === 'file') { const outputPropertyName = this.getNodeParameter( 'options.response.response.outputPropertyName', 0, 'data', ) as string; const newItem: INodeExecutionData = { json: {}, binary: {}, pairedItem: { item: itemIndex, }, }; if (items[itemIndex].binary !== undefined) { // Create a shallow copy of the binary data so that the old // data references which do not get changed still stay behind // but the incoming data does not get changed. Object.assign(newItem.binary as IBinaryKeyData, items[itemIndex].binary); } let binaryData: Buffer | Readable; if (fullResponse) { const returnItem: IDataObject = {}; for (const property of fullResponseProperties) { if (property === 'body') { continue; } returnItem[property] = response[property]; } newItem.json = returnItem; binaryData = response?.body; } else { newItem.json = items[itemIndex].json; binaryData = response; } const preparedBinaryData = await this.helpers.prepareBinaryData( binaryData, undefined, responseContentType || undefined, ); if ( !preparedBinaryData.fileName && preparedBinaryData.fileExtension && typeof requestOptions.uri === 'string' && requestOptions.uri.endsWith(preparedBinaryData.fileExtension) ) { preparedBinaryData.fileName = requestOptions.uri.split('/').pop(); } newItem.binary![outputPropertyName] = preparedBinaryData; returnItems.push(newItem); } else if (responseFormat === 'text') { const outputPropertyName = this.getNodeParameter( 'options.response.response.outputPropertyName', 0, 'data', ) as string; if (fullResponse) { const returnItem: IDataObject = {}; for (const property of fullResponseProperties) { if (property === 'body') { returnItem[outputPropertyName] = toText(response[property]); continue; } returnItem[property] = response[property]; } returnItems.push({ json: returnItem, pairedItem: { item: itemIndex, }, }); } else { returnItems.push({ json: { [outputPropertyName]: toText(response), }, pairedItem: { item: itemIndex, }, }); } } else { // responseFormat: 'json' if (fullResponse) { const returnItem: IDataObject = {}; for (const property of fullResponseProperties) { returnItem[property] = response[property]; } if (responseFormat === 'json' && typeof returnItem.body === 'string') { try { returnItem.body = JSON.parse(returnItem.body); } catch (error) { throw new NodeOperationError( this.getNode(), 'Response body is not valid JSON. Change "Response Format" to "Text"', { itemIndex }, ); } } returnItems.push({ json: returnItem, pairedItem: { item: itemIndex, }, }); } else { if (responseFormat === 'json' && typeof response === 'string') { try { if (typeof response !== 'object') { response = JSON.parse(response); } } catch (error) { throw new NodeOperationError( this.getNode(), 'Response body is not valid JSON. Change "Response Format" to "Text"', { itemIndex }, ); } } if (Array.isArray(response)) { // eslint-disable-next-line @typescript-eslint/no-loop-func response.forEach((item) => returnItems.push({ json: item, pairedItem: { item: itemIndex, }, }), ); } else { returnItems.push({ json: response, pairedItem: { item: itemIndex, }, }); } } } } } returnItems = returnItems.map(replaceNullValues); if ( returnItems.length === 1 && returnItems[0].json.data && Array.isArray(returnItems[0].json.data) ) { return new NodeExecutionOutput( [returnItems], [ { message: 'To split the contents of ‘data’ into separate items for easier processing, add a ‘Spilt Out’ node after this one', location: 'outputPane', }, ], ); } return [returnItems]; } }