From c3c8a580c57e852c358e63b1823373a89cbc2c84 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 4 Feb 2025 14:49:38 +0100 Subject: [PATCH] feat(HTTP Request Tool Node): Relax binary data detection --- .../test/ToolHttpRequest.node.test.ts | 63 +++++++++++++++++++ .../nodes/tools/ToolHttpRequest/utils.ts | 29 ++++++--- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts index 05ed1e619c2ac..346234c8d3831 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts @@ -237,6 +237,69 @@ describe('ToolHttpRequest', () => { }), ); }); + + it('should return the error when receiving text that contains a null character', async () => { + helpers.httpRequest.mockResolvedValue({ + body: 'Hello\0World', + headers: { + 'content-type': 'text/plain', + }, + }); + + executeFunctions.getNodeParameter.mockImplementation((paramName: string) => { + switch (paramName) { + case 'method': + return 'GET'; + case 'url': + return 'https://httpbin.org/text/plain'; + case 'options': + return {}; + case 'placeholderDefinitions.values': + return []; + default: + return undefined; + } + }); + + const { response } = await httpTool.supplyData.call(executeFunctions, 0); + const res = await (response as N8nTool).invoke({}); + expect(helpers.httpRequest).toHaveBeenCalled(); + // Check that the returned string is formatted as an error message. + expect(res).toContain('error'); + expect(res).toContain('Binary data is not supported'); + }); + + it('should return the error when receiving a JSON response containing a null character', async () => { + // Provide a raw JSON string with a literal null character. + helpers.httpRequest.mockResolvedValue({ + body: '{"message":"hello\0world"}', + headers: { + 'content-type': 'application/json', + }, + }); + + executeFunctions.getNodeParameter.mockImplementation((paramName: string) => { + switch (paramName) { + case 'method': + return 'GET'; + case 'url': + return 'https://httpbin.org/json'; + case 'options': + return {}; + case 'placeholderDefinitions.values': + return []; + default: + return undefined; + } + }); + + const { response } = await httpTool.supplyData.call(executeFunctions, 0); + const res = await (response as N8nTool).invoke({}); + expect(helpers.httpRequest).toHaveBeenCalled(); + // Check that the tool returns an error string rather than resolving to valid JSON. + expect(res).toContain('error'); + expect(res).toContain('Binary data is not supported'); + }); }); describe('Optimize response', () => { diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts index f1d6dfd150483..0bd1b1a8a6ba7 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts @@ -5,7 +5,6 @@ import { JSDOM } from 'jsdom'; import get from 'lodash/get'; import set from 'lodash/set'; import unset from 'lodash/unset'; -import * as mime from 'mime-types'; import { getOAuth2AdditionalParameters } from 'n8n-nodes-base/dist/nodes/HttpRequest/GenericFunctions'; import type { IDataObject, @@ -146,6 +145,25 @@ const defaultOptimizer = (response: T) => { return String(response); }; +function isBinary(data: unknown) { + // Check if data is a Buffer + if (Buffer.isBuffer(data)) { + return true; + } + + // If data is a string, assume it's text unless it contains null characters. + if (typeof data === 'string') { + // If the string contains a null character, it's likely binary. + if (data.includes('\0')) { + return true; + } + return false; + } + + // For any other type, assume it's not binary. + return false; +} + const htmlOptimizer = (ctx: ISupplyDataFunctions, itemIndex: number, maxLength: number) => { const cssSelector = ctx.getNodeParameter('cssSelector', itemIndex, '') as string; const onlyContent = ctx.getNodeParameter('onlyContent', itemIndex, false) as boolean; @@ -755,13 +773,8 @@ export const configureToolFunction = ( if (!response) { try { // Check if the response is binary data - if (fullResponse?.headers?.['content-type']) { - const contentType = fullResponse.headers['content-type'] as string; - const mimeType = contentType.split(';')[0].trim(); - - if (mime.charset(mimeType) !== 'UTF-8') { - throw new NodeOperationError(ctx.getNode(), 'Binary data is not supported'); - } + if (fullResponse.body && isBinary(fullResponse.body)) { + throw new NodeOperationError(ctx.getNode(), 'Binary data is not supported'); } response = optimizeResponse(fullResponse.body);