diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index 9ffc87b6f4..a1eaa763d3 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -748,276 +748,289 @@ export class HttpRequestV3 implements INodeType { let responseData: any; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - responseData = promisesResponses.shift(); + try { + 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 = - "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 + if (errorItems[itemIndex]) { returnItems.push({ - json: { - error: responseData.reason, - }, - pairedItem: { - item: itemIndex, - }, + json: { error: errorItems[itemIndex] }, + 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; + if (responseData!.status !== 'fulfilled') { + if (responseData.reason.statusCode === 429) { + responseData.reason.message = + "Try spacing your requests out using the batching settings under 'Options'"; } - } - - 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; - } - } - } - // This is a no-op outside of tool usage - const optimizeResponse = configureResponseOptimizer(this, itemIndex); - - if (autoDetectResponseFormat && !fullResponse) { - delete response.headers; - delete response.statusCode; - delete response.statusMessage; - } - if (!fullResponse) { - response = optimizeResponse(response.body); - } else { - response.body = optimizeResponse(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]; + if (!this.continueOnFail()) { + if (autoDetectResponseFormat && responseData.reason.error instanceof Buffer) { + responseData.reason.error = Buffer.from( + responseData.reason.error as Buffer, + ).toString(); } - newItem.json = returnItem; - binaryData = response?.body; - } else { - newItem.json = items[itemIndex].json; - binaryData = response; - } - const preparedBinaryData = await this.helpers.prepareBinaryData( - binaryData, - undefined, - mimeTypeFromResponse(responseContentType), - ); - - preparedBinaryData.fileName = setFilename( - preparedBinaryData, - requestOptions, - responseFileName, - ); - - 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]; + 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 }); } - returnItems.push({ - json: returnItem, - pairedItem: { - item: itemIndex, - }, - }); + + set(error, 'context.request', sanitizedRequests[itemIndex]); + + throw error; } else { + removeCircularRefs(responseData.reason as JsonObject); + // Return the actual reason as error returnItems.push({ json: { - [outputPropertyName]: toText(response), + error: responseData.reason, }, pairedItem: { item: itemIndex, }, }); + continue; } - } 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 }, - ); + 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; } } + } + // This is a no-op outside of tool usage + const optimizeResponse = configureResponseOptimizer(this, itemIndex); - returnItems.push({ - json: returnItem, + if (autoDetectResponseFormat && !fullResponse) { + delete response.headers; + delete response.statusCode; + delete response.statusMessage; + } + if (!fullResponse) { + response = optimizeResponse(response.body); + } else { + response.body = optimizeResponse(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, }, - }); - } 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 (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); } - if (Array.isArray(response)) { - response.forEach((item) => - returnItems.push({ - json: item, - pairedItem: { - item: itemIndex, - }, - }), - ); + 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, + mimeTypeFromResponse(responseContentType), + ); + + preparedBinaryData.fileName = setFilename( + preparedBinaryData, + requestOptions, + responseFileName, + ); + + 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: response, + 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)) { + response.forEach((item) => + returnItems.push({ + json: item, + pairedItem: { + item: itemIndex, + }, + }), + ); + } else { + returnItems.push({ + json: response, + pairedItem: { + item: itemIndex, + }, + }); + } + } } } + } catch (error) { + if (!this.continueOnFail()) throw error; + + returnItems.push({ + json: { error: error.message }, + pairedItem: { item: itemIndex }, + }); + + continue; } } diff --git a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequest.test.ts b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequest.test.ts index e0292feedf..d0dbc43b30 100644 --- a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequest.test.ts +++ b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequest.test.ts @@ -106,6 +106,7 @@ describe('Test HTTP Request Node', () => { completed: false, userId: 15, }); + nock(baseUrl).get('/html').reply(200, '

Test

'); //PUT nock(baseUrl).put('/todos/10', { userId: '42' }).reply(200, { 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 index e96710cbc8..d83519fcfb 100644 --- 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 @@ -3,53 +3,95 @@ "nodes": [ { "parameters": {}, - "id": "6e15f2de-79fe-41f3-b76e-53ebfa2e4437", + "id": "6707decf-7ae3-46f1-8603-b0fe4844f240", "name": "When clicking ‘Execute workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, - "position": [-60, 580] + "position": [16, 560] }, { "parameters": {}, - "id": "af1bb989-3c6d-4323-8e88-649e45d64c00", + "id": "09b63a84-789d-4a31-b546-849ba47ed689", "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] + "position": [464, 272] }, { "parameters": { "method": "POST", - "url": "https://webhook.site/e18fe8f9-ec77-4574-a40d-8ae054191e1e", + "url": "https://dummyjson.com/todos/1", "sendBody": true, "specifyBody": "json", "jsonBody": "{\n \"q\": \"abc\",\n}", "options": {} }, - "id": "31641920-7f43-473f-ad96-b121122802bb", - "name": "HTTP Request", + "id": "c6841eb1-7913-4c8d-8c9d-b88a908125ed", + "name": "Invalid JSON Body", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [180, 580], + "position": [240, 368], "alwaysOutputData": false, "onError": "continueErrorOutput" + }, + { + "parameters": {}, + "id": "21750b7e-c18a-43a5-a068-f8aa85d1cadf", + "name": "Success path1", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [464, 656] + }, + { + "parameters": { + "url": "https://dummyjson.com/html", + "options": { + "response": { + "response": { + "responseFormat": "json" + } + } + } + }, + "id": "ae2891f0-1968-4f57-a1a0-87d6f6dab57d", + "name": "Invalid JSON Response", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [240, 752], + "alwaysOutputData": false, + "onError": "continueErrorOutput" + }, + { + "parameters": {}, + "id": "baa75f00-e0dd-40b2-b718-1e8311549a05", + "name": "Request body error", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [464, 464] + }, + { + "parameters": {}, + "id": "4733c47f-38c8-44a8-9d77-d9e733777cbf", + "name": "Response body error", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [464, 848] } ], "pinData": { - "Error path": [ + "Request body error": [ { "json": { "error": "JSON parameter needs to be valid JSON" } } + ], + "Response body error": [ + { + "json": { + "error": "Response body is not valid JSON. Change \"Response Format\" to \"Text\"" + } + } ] }, "connections": { @@ -57,14 +99,19 @@ "main": [ [ { - "node": "HTTP Request", + "node": "Invalid JSON Body", + "type": "main", + "index": 0 + }, + { + "node": "Invalid JSON Response", "type": "main", "index": 0 } ] ] }, - "HTTP Request": { + "Invalid JSON Body": { "main": [ [ { @@ -75,7 +122,25 @@ ], [ { - "node": "Error path", + "node": "Request body error", + "type": "main", + "index": 0 + } + ] + ] + }, + "Invalid JSON Response": { + "main": [ + [ + { + "node": "Success path1", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Response body error", "type": "main", "index": 0 } @@ -87,11 +152,10 @@ "settings": { "executionOrder": "v1" }, - "versionId": "f445a6e9-818f-4b70-8b4f-e2a68fc5d6e4", + "versionId": "c7026310-8c4f-4889-a45b-befebacc7dde", "meta": { - "templateCredsSetupCompleted": true, - "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" }, - "id": "f3rDILaMkFqisP3P", + "id": "2Zd3If2j9PglrHri", "tags": [] }