Skip to content

Commit

Permalink
fix(HTTP Request Tool Node): Respond with an error when receive binar…
Browse files Browse the repository at this point in the history
…y response (n8n-io#11219)
  • Loading branch information
burivuhster authored Oct 11, 2024
1 parent 8734dc9 commit 0d23a7f
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export class ToolHttpRequest implements INodeType {
'User-Agent': undefined,
},
body: {},
returnFullResponse: true,
};

const authentication = this.getNodeParameter('authentication', itemIndex, 'none') as
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
48 changes: 32 additions & 16 deletions packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -176,6 +174,7 @@ const htmlOptimizer = (ctx: IExecuteFunctions, itemIndex: number, maxLength: num
);
}
const returnData: string[] = [];

const html = cheerio.load(response);
const htmlElements = html(cssSelector);

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -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') {
Expand Down
2 changes: 2 additions & 0 deletions packages/@n8n/nodes-langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
Expand Down Expand Up @@ -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",
Expand Down
18 changes: 12 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0d23a7f

Please sign in to comment.