diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts index 32f6be42e74b6..aa294edadd612 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.ts @@ -281,6 +281,7 @@ export class ToolHttpRequest implements INodeType { 'User-Agent': undefined, }, body: {}, + returnFullResponse: true, }; const authentication = this.getNodeParameter('authentication', itemIndex, 'none') as 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 new file mode 100644 index 0000000000000..161aa140f54de --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts @@ -0,0 +1,165 @@ +import get from 'lodash/get'; +import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; +import { jsonParse } from 'n8n-workflow'; + +import type { N8nTool } from '../../../../utils/N8nTool'; +import { ToolHttpRequest } from '../ToolHttpRequest.node'; + +const createExecuteFunctionsMock = (parameters: IDataObject, requestMock: any) => { + const nodeParameters = parameters; + + return { + getNodeParameter(parameter: string) { + return get(nodeParameters, parameter); + }, + getNode() { + return { + name: 'HTTP Request', + }; + }, + getInputData() { + return [{ json: {} }]; + }, + getWorkflow() { + return { + name: 'Test Workflow', + }; + }, + continueOnFail() { + return false; + }, + addInputData() { + return { index: 0 }; + }, + addOutputData() { + return; + }, + helpers: { + httpRequest: requestMock, + }, + } as unknown as IExecuteFunctions; +}; + +describe('ToolHttpRequest', () => { + let httpTool: ToolHttpRequest; + let mockRequest: jest.Mock; + + describe('Binary response', () => { + beforeEach(() => { + httpTool = new ToolHttpRequest(); + mockRequest = jest.fn(); + }); + + it('should return the error when receiving a binary response', async () => { + mockRequest.mockResolvedValue({ + body: Buffer.from(''), + headers: { + 'content-type': 'image/jpeg', + }, + }); + + const { response } = await httpTool.supplyData.call( + createExecuteFunctionsMock( + { + method: 'GET', + url: 'https://httpbin.org/image/jpeg', + options: {}, + placeholderDefinitions: { + values: [], + }, + }, + mockRequest, + ), + 0, + ); + + const res = await (response as N8nTool).invoke(''); + + expect(res).toContain('error'); + expect(res).toContain('Binary data is not supported'); + }); + + it('should return the response text when receiving a text response', async () => { + mockRequest.mockResolvedValue({ + body: 'Hello World', + headers: { + 'content-type': 'text/plain', + }, + }); + + const { response } = await httpTool.supplyData.call( + createExecuteFunctionsMock( + { + method: 'GET', + url: 'https://httpbin.org/text/plain', + options: {}, + placeholderDefinitions: { + values: [], + }, + }, + mockRequest, + ), + 0, + ); + + const res = await (response as N8nTool).invoke(''); + expect(res).toEqual('Hello World'); + }); + + it('should return the response text when receiving a text response with a charset', async () => { + mockRequest.mockResolvedValue({ + body: 'こんにちは世界', + headers: { + 'content-type': 'text/plain; charset=iso-2022-jp', + }, + }); + + const { response } = await httpTool.supplyData.call( + createExecuteFunctionsMock( + { + method: 'GET', + url: 'https://httpbin.org/text/plain', + options: {}, + placeholderDefinitions: { + values: [], + }, + }, + mockRequest, + ), + 0, + ); + + const res = await (response as N8nTool).invoke(''); + expect(res).toEqual('こんにちは世界'); + }); + + it('should return the response object when receiving a JSON response', async () => { + const mockJson = { hello: 'world' }; + + mockRequest.mockResolvedValue({ + body: mockJson, + headers: { + 'content-type': 'application/json', + }, + }); + + const { response } = await httpTool.supplyData.call( + createExecuteFunctionsMock( + { + method: 'GET', + url: 'https://httpbin.org/json', + options: {}, + placeholderDefinitions: { + values: [], + }, + }, + mockRequest, + ), + 0, + ); + + const res = await (response as N8nTool).invoke(''); + expect(jsonParse(res)).toEqual(mockJson); + }); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts index c06a869a8dc71..e637251a74c7f 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts @@ -1,3 +1,12 @@ +import { Readability } from '@mozilla/readability'; +import cheerio from 'cheerio'; +import { convert } from 'html-to-text'; +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 { IExecuteFunctions, IDataObject, @@ -7,20 +16,8 @@ import type { NodeApiError, } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow'; - -import { getOAuth2AdditionalParameters } from 'n8n-nodes-base/dist/nodes/HttpRequest/GenericFunctions'; - -import set from 'lodash/set'; -import get from 'lodash/get'; -import unset from 'lodash/unset'; - -import cheerio from 'cheerio'; -import { convert } from 'html-to-text'; - -import { Readability } from '@mozilla/readability'; -import { JSDOM } from 'jsdom'; import { z } from 'zod'; -import type { DynamicZodObject } from '../../../types/zod.types'; + import type { ParameterInputType, ParametersValues, @@ -29,6 +26,7 @@ import type { SendIn, ToolParameter, } from './interfaces'; +import type { DynamicZodObject } from '../../../types/zod.types'; const genericCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: number) => { const genericType = ctx.getNodeParameter('genericAuthType', itemIndex) as string; @@ -176,6 +174,7 @@ const htmlOptimizer = (ctx: IExecuteFunctions, itemIndex: number, maxLength: num ); } const returnData: string[] = []; + const html = cheerio.load(response); const htmlElements = html(cssSelector); @@ -574,6 +573,7 @@ export const configureToolFunction = ( // Clone options and rawRequestOptions to avoid mutating the original objects const options: IHttpRequestOptions | null = structuredClone(requestOptions); const clonedRawRequestOptions: { [key: string]: string } = structuredClone(rawRequestOptions); + let fullResponse: any; let response: string = ''; let executionError: Error | undefined = undefined; @@ -732,8 +732,6 @@ export const configureToolFunction = ( } } } catch (error) { - console.error(error); - const errorMessage = 'Input provided by model is not valid'; if (error instanceof NodeOperationError) { @@ -749,11 +747,29 @@ export const configureToolFunction = ( if (options) { try { - response = optimizeResponse(await httpRequest(options)); + fullResponse = await httpRequest(options); } catch (error) { const httpCode = (error as NodeApiError).httpCode; response = `${httpCode ? `HTTP ${httpCode} ` : ''}There was an error: "${error.message}"`; } + + 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'); + } + } + + response = optimizeResponse(fullResponse.body); + } catch (error) { + response = `There was an error: "${error.message}"`; + } + } } if (typeof response !== 'string') { diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 4df337ed52268..d3da518f10909 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -124,6 +124,7 @@ "@types/cheerio": "^0.22.15", "@types/html-to-text": "^9.0.1", "@types/json-schema": "^7.0.15", + "@types/mime-types": "^2.1.0", "@types/pg": "^8.11.6", "@types/temp": "^0.9.1", "n8n-core": "workspace:*" @@ -171,6 +172,7 @@ "langchain": "0.3.2", "lodash": "catalog:", "mammoth": "1.7.2", + "mime-types": "2.1.35", "n8n-nodes-base": "workspace:*", "n8n-workflow": "workspace:*", "openai": "4.63.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dca3aee05c1d..674809114822d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -522,6 +522,9 @@ importers: mammoth: specifier: 1.7.2 version: 1.7.2 + mime-types: + specifier: 2.1.35 + version: 2.1.35 n8n-nodes-base: specifier: workspace:* version: link:../../nodes-base @@ -568,6 +571,9 @@ importers: '@types/json-schema': specifier: ^7.0.15 version: 7.0.15 + '@types/mime-types': + specifier: ^2.1.0 + version: 2.1.1 '@types/pg': specifier: ^8.11.6 version: 8.11.6 @@ -19287,7 +19293,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -19312,7 +19318,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 @@ -19332,7 +19338,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -20130,7 +20136,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -23033,7 +23039,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -23862,7 +23868,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color