diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index 7ef4b5b3b6ec5..f24f537c91e97 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -106,6 +106,7 @@ export class HttpRequestV3 implements INodeType { }; let returnItems: INodeExecutionData[] = []; + const errorItems: { [key: string]: string } = {}; const requestPromises = []; let fullResponse = false; @@ -140,203 +141,270 @@ export class HttpRequestV3 implements INodeType { }> = []; 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); + try { + 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; } - } else if (authentication === 'predefinedCredentialType') { - nodeCredentialType = this.getNodeParameter('nodeCredentialType', itemIndex) as string; - } - const provideSslCertificates = this.getNodeParameter( - 'provideSslCertificates', - itemIndex, - false, - ); + const provideSslCertificates = this.getNodeParameter( + 'provideSslCertificates', + itemIndex, + false, + ); - if (provideSslCertificates) { - sslCertificates = await this.getCredentials('httpSslAuth', itemIndex); - } + 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 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 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 }; + 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; }; - redirect: { redirect: { maxRedirects: number; followRedirects: boolean } }; - lowercaseHeaders: boolean; - }; - const url = this.getNodeParameter('url', itemIndex) as string; + const url = this.getNodeParameter('url', itemIndex) as string; - const responseFormat = response?.response?.responseFormat || 'autodetect'; + const responseFormat = response?.response?.responseFormat || 'autodetect'; - fullResponse = response?.response?.fullResponse || false; + fullResponse = response?.response?.fullResponse || false; - autoDetectResponseFormat = responseFormat === 'autodetect'; + 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; + // 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); + 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, - }; + 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 }; - } + if (requestOptions.method !== 'GET' && nodeVersion >= 4.1) { + requestOptions = { ...requestOptions, followAllRedirects: false }; + } - const defaultRedirect = nodeVersion >= 4 && redirect === undefined; + const defaultRedirect = nodeVersion >= 4 && redirect === undefined; - if (redirect?.redirect?.followRedirects || defaultRedirect) { - requestOptions.followRedirect = true; - requestOptions.followAllRedirects = true; - } + if (redirect?.redirect?.followRedirects || defaultRedirect) { + requestOptions.followRedirect = true; + requestOptions.followAllRedirects = true; + } - if (redirect?.redirect?.maxRedirects || defaultRedirect) { - requestOptions.maxRedirects = redirect?.redirect?.maxRedirects; - } + if (redirect?.redirect?.maxRedirects || defaultRedirect) { + requestOptions.maxRedirects = redirect?.redirect?.maxRedirects; + } - if (response?.response?.neverError) { - requestOptions.simple = false; - } + if (response?.response?.neverError) { + requestOptions.simple = false; + } - if (proxy) { - requestOptions.proxy = proxy; - } + 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 }, - }); - } + 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); - } + 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, - }, - }; + 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 needs to be valid JSON', + { + itemIndex, + }, + ); + } + + requestOptions.body = jsonParse(jsonBodyParameter); + } else { + requestOptions.body = jsonBodyParameter; + } + } else if (specifyBody === 'string') { + //form urlencoded + requestOptions.body = Object.fromEntries(new URLSearchParams(body)); + } } - 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) { + // 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(jsonBodyParameter); + JSON.parse(jsonQueryParameter); } catch { throw new NodeOperationError( this.getNode(), @@ -347,316 +415,280 @@ export class HttpRequestV3 implements INodeType { ); } - requestOptions.body = jsonParse(jsonBodyParameter); - } else { - requestOptions.body = jsonBodyParameter; + requestOptions.qs = jsonParse(jsonQueryParameter); } - } 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); + // 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 needs to be valid JSON', + { + itemIndex, + }, + ); + } - 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; + additionalHeaders = jsonParse(jsonHeadersParameter); } - requestOptions.body = uploadData; requestOptions.headers = { ...requestOptions.headers, - 'content-length': contentLength, - 'content-type': itemBinaryData.mimeType ?? 'application/octet-stream', + ...(lowercaseHeaders === undefined || lowercaseHeaders + ? keysToLowercase(additionalHeaders) + : additionalHeaders), }; - } 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 needs to be valid JSON', { - itemIndex, - }); - } - - requestOptions.qs = jsonParse(jsonQueryParameter); + 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; } - } - // 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 needs to be valid JSON', { - itemIndex, - }); + // Add Content Type if any are set + if (bodyContentType === 'raw') { + if (requestOptions.headers === undefined) { + requestOptions.headers = {}; } - - additionalHeaders = jsonParse(jsonHeadersParameter); + const rawContentType = this.getNodeParameter('rawContentType', itemIndex) as string; + requestOptions.headers['content-type'] = rawContentType; } - 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; - } + const authDataKeys: IAuthDataSanitizeKeys = {}; - // Add Content Type if any are set - if (bodyContentType === 'raw') { - if (requestOptions.headers === undefined) { - requestOptions.headers = {}; + // Add SSL certificates if any are set + setAgentOptions(requestOptions, sslCertificates); + if (requestOptions.agentOptions) { + authDataKeys.agentOptions = Object.keys(requestOptions.agentOptions); } - 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); + // 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 (customAuth.body) { - requestOptions.body = { ...(requestOptions.body as IDataObject), ...customAuth.body }; - authDataKeys.body = Object.keys(customAuth.body); + if (httpHeaderAuth !== undefined) { + requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value; + authDataKeys.headers = [httpHeaderAuth.name as string]; } - if (customAuth.qs) { - requestOptions.qs = { ...requestOptions.qs, ...customAuth.qs }; - authDataKeys.qs = Object.keys(customAuth.qs); + if (httpQueryAuth !== undefined) { + if (!requestOptions.qs) { + requestOptions.qs = {}; + } + requestOptions.qs[httpQueryAuth.name as string] = httpQueryAuth.value; + authDataKeys.qs = [httpQueryAuth.name as string]; } - } - 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'; + 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); + } } - } - 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'); + 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'; } - continueExpression = `={{ !(${pagination.completeExpression.trim().slice(3, -2)}) }}`; } - const paginationData: PaginationOptions = { - continue: continueExpression, - request: {}, - requestInterval: pagination.requestInterval, - }; + requests.push({ + options: requestOptions, + authKeys: authDataKeys, + credentialType: nodeCredentialType, + }); - 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`, - ); + 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'); } - const parameterValue = parameter.value; - if (parameterValue === '') { + 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(), - `Some value must be provided for parameter [${ - index + 1 - }] in pagination settings, omitting it will result in an infinite loop`, + "At least one entry with 'Name' and 'Value' filled must be included in 'Parameters' to use 'Update a Parameter in Each Request' mode ", ); } - paginationData.request[parameter.type]![parameterName] = parameterValue; - }); - } else if (pagination.paginationMode === 'responseContainsNextURL') { - paginationData.request.url = pagination.nextURL; - } + 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 (pagination.limitPagesFetched) { + paginationData.maxRequests = pagination.maxRequests; + } - if (responseFormat === 'file') { - paginationData.binaryResult = true; - } + 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 requestPromise = this.helpers.requestWithAuthenticationPaginated - .call( + const requestWithAuthentication = this.helpers.requestWithAuthentication.call( this, + nodeCredentialType, requestOptions, + additionalOAuth2Options && { oauth2: additionalOAuth2Options }, 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); + ); + requestWithAuthentication.catch(() => {}); + requestPromises.push(requestWithAuthentication); } - } else if (authentication === 'predefinedCredentialType' && nodeCredentialType) { - const additionalOAuth2Options = getOAuth2AdditionalParameters(nodeCredentialType); + } catch (error) { + if (!this.continueOnFail()) throw error; - // service-specific cred: OAuth1, OAuth2, plain + requestPromises.push(Promise.reject(error).catch(() => {})); - const requestWithAuthentication = this.helpers.requestWithAuthentication.call( - this, - nodeCredentialType, - requestOptions, - additionalOAuth2Options && { oauth2: additionalOAuth2Options }, - itemIndex, - ); - requestWithAuthentication.catch(() => {}); - requestPromises.push(requestWithAuthentication); + errorItems[itemIndex] = error.message; + + continue; } } @@ -664,28 +696,41 @@ export class HttpRequestV3 implements INodeType { 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) {} - }), + await requestPromise + .then((response) => response) + .finally(async () => { + if (errorItems[itemIndex]) return; + 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 (errorItems[itemIndex]) { + returnItems.push({ + json: { error: errorItems[itemIndex] }, + pairedItem: { item: itemIndex }, + }); + + continue; + } + if (responseData!.status !== 'fulfilled') { if (responseData.reason.statusCode === 429) { responseData.reason.message = diff --git a/packages/nodes-base/nodes/HttpRequest/test/node/workflow.use_error_output.json b/packages/nodes-base/nodes/HttpRequest/test/node/workflow.use_error_output.json new file mode 100644 index 0000000000000..c8d79e673d53e --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/test/node/workflow.use_error_output.json @@ -0,0 +1,97 @@ +{ + "name": "HTTP Request Node: Continue using error output not working", + "nodes": [ + { + "parameters": {}, + "id": "6e15f2de-79fe-41f3-b76e-53ebfa2e4437", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-60, 580] + }, + { + "parameters": {}, + "id": "af1bb989-3c6d-4323-8e88-649e45d64c00", + "name": "Success path", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [460, 460] + }, + { + "parameters": {}, + "id": "43c4ee19-6a9d-4b1d-aefa-2c24abe45189", + "name": "Error path", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [460, 700] + }, + { + "parameters": { + "method": "POST", + "url": "https://webhook.site/e18fe8f9-ec77-4574-a40d-8ae054191e1e", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "{\n \"q\": \"abc\",\n}", + "options": {} + }, + "id": "31641920-7f43-473f-ad96-b121122802bb", + "name": "HTTP Request", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [180, 580], + "alwaysOutputData": false, + "onError": "continueErrorOutput" + } + ], + "pinData": { + "Error path": [ + { + "json": { + "error": "JSON parameter needs to be valid JSON" + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request": { + "main": [ + [ + { + "node": "Success path", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Error path", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "f445a6e9-818f-4b70-8b4f-e2a68fc5d6e4", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + }, + "id": "f3rDILaMkFqisP3P", + "tags": [] +}