mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 05:17:28 -08:00
fix(HTTP Request Tool Node): Respond with an error when receive binary response (#11219)
Some checks failed
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Has been cancelled
Some checks failed
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Has been cancelled
This commit is contained in:
parent
8734dc99fc
commit
0d23a7fb5b
|
@ -281,6 +281,7 @@ export class ToolHttpRequest implements INodeType {
|
||||||
'User-Agent': undefined,
|
'User-Agent': undefined,
|
||||||
},
|
},
|
||||||
body: {},
|
body: {},
|
||||||
|
returnFullResponse: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const authentication = this.getNodeParameter('authentication', itemIndex, 'none') as
|
const authentication = this.getNodeParameter('authentication', itemIndex, 'none') as
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 {
|
import type {
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
@ -7,20 +16,8 @@ import type {
|
||||||
NodeApiError,
|
NodeApiError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeOperationError, jsonParse } 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 { z } from 'zod';
|
||||||
import type { DynamicZodObject } from '../../../types/zod.types';
|
|
||||||
import type {
|
import type {
|
||||||
ParameterInputType,
|
ParameterInputType,
|
||||||
ParametersValues,
|
ParametersValues,
|
||||||
|
@ -29,6 +26,7 @@ import type {
|
||||||
SendIn,
|
SendIn,
|
||||||
ToolParameter,
|
ToolParameter,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
|
import type { DynamicZodObject } from '../../../types/zod.types';
|
||||||
|
|
||||||
const genericCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: number) => {
|
const genericCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: number) => {
|
||||||
const genericType = ctx.getNodeParameter('genericAuthType', itemIndex) as string;
|
const genericType = ctx.getNodeParameter('genericAuthType', itemIndex) as string;
|
||||||
|
@ -176,6 +174,7 @@ const htmlOptimizer = (ctx: IExecuteFunctions, itemIndex: number, maxLength: num
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const returnData: string[] = [];
|
const returnData: string[] = [];
|
||||||
|
|
||||||
const html = cheerio.load(response);
|
const html = cheerio.load(response);
|
||||||
const htmlElements = html(cssSelector);
|
const htmlElements = html(cssSelector);
|
||||||
|
|
||||||
|
@ -574,6 +573,7 @@ export const configureToolFunction = (
|
||||||
// Clone options and rawRequestOptions to avoid mutating the original objects
|
// Clone options and rawRequestOptions to avoid mutating the original objects
|
||||||
const options: IHttpRequestOptions | null = structuredClone(requestOptions);
|
const options: IHttpRequestOptions | null = structuredClone(requestOptions);
|
||||||
const clonedRawRequestOptions: { [key: string]: string } = structuredClone(rawRequestOptions);
|
const clonedRawRequestOptions: { [key: string]: string } = structuredClone(rawRequestOptions);
|
||||||
|
let fullResponse: any;
|
||||||
let response: string = '';
|
let response: string = '';
|
||||||
let executionError: Error | undefined = undefined;
|
let executionError: Error | undefined = undefined;
|
||||||
|
|
||||||
|
@ -732,8 +732,6 @@ export const configureToolFunction = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
const errorMessage = 'Input provided by model is not valid';
|
const errorMessage = 'Input provided by model is not valid';
|
||||||
|
|
||||||
if (error instanceof NodeOperationError) {
|
if (error instanceof NodeOperationError) {
|
||||||
|
@ -749,11 +747,29 @@ export const configureToolFunction = (
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
try {
|
try {
|
||||||
response = optimizeResponse(await httpRequest(options));
|
fullResponse = await httpRequest(options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const httpCode = (error as NodeApiError).httpCode;
|
const httpCode = (error as NodeApiError).httpCode;
|
||||||
response = `${httpCode ? `HTTP ${httpCode} ` : ''}There was an error: "${error.message}"`;
|
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') {
|
if (typeof response !== 'string') {
|
||||||
|
|
|
@ -124,6 +124,7 @@
|
||||||
"@types/cheerio": "^0.22.15",
|
"@types/cheerio": "^0.22.15",
|
||||||
"@types/html-to-text": "^9.0.1",
|
"@types/html-to-text": "^9.0.1",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"@types/mime-types": "^2.1.0",
|
||||||
"@types/pg": "^8.11.6",
|
"@types/pg": "^8.11.6",
|
||||||
"@types/temp": "^0.9.1",
|
"@types/temp": "^0.9.1",
|
||||||
"n8n-core": "workspace:*"
|
"n8n-core": "workspace:*"
|
||||||
|
@ -171,6 +172,7 @@
|
||||||
"langchain": "0.3.2",
|
"langchain": "0.3.2",
|
||||||
"lodash": "catalog:",
|
"lodash": "catalog:",
|
||||||
"mammoth": "1.7.2",
|
"mammoth": "1.7.2",
|
||||||
|
"mime-types": "2.1.35",
|
||||||
"n8n-nodes-base": "workspace:*",
|
"n8n-nodes-base": "workspace:*",
|
||||||
"n8n-workflow": "workspace:*",
|
"n8n-workflow": "workspace:*",
|
||||||
"openai": "4.63.0",
|
"openai": "4.63.0",
|
||||||
|
|
|
@ -522,6 +522,9 @@ importers:
|
||||||
mammoth:
|
mammoth:
|
||||||
specifier: 1.7.2
|
specifier: 1.7.2
|
||||||
version: 1.7.2
|
version: 1.7.2
|
||||||
|
mime-types:
|
||||||
|
specifier: 2.1.35
|
||||||
|
version: 2.1.35
|
||||||
n8n-nodes-base:
|
n8n-nodes-base:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../nodes-base
|
version: link:../../nodes-base
|
||||||
|
@ -568,6 +571,9 @@ importers:
|
||||||
'@types/json-schema':
|
'@types/json-schema':
|
||||||
specifier: ^7.0.15
|
specifier: ^7.0.15
|
||||||
version: 7.0.15
|
version: 7.0.15
|
||||||
|
'@types/mime-types':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.1
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: ^8.11.6
|
specifier: ^8.11.6
|
||||||
version: 8.11.6
|
version: 8.11.6
|
||||||
|
@ -19287,7 +19293,7 @@ snapshots:
|
||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.9:
|
||||||
dependencies:
|
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
|
is-core-module: 2.13.1
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
transitivePeerDependencies:
|
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):
|
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:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.6.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.6.2)
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
|
@ -19332,7 +19338,7 @@ snapshots:
|
||||||
array.prototype.findlastindex: 1.2.3
|
array.prototype.findlastindex: 1.2.3
|
||||||
array.prototype.flat: 1.3.2
|
array.prototype.flat: 1.3.2
|
||||||
array.prototype.flatmap: 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
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
|
@ -20130,7 +20136,7 @@ snapshots:
|
||||||
array-parallel: 0.1.3
|
array-parallel: 0.1.3
|
||||||
array-series: 0.1.5
|
array-series: 0.1.5
|
||||||
cross-spawn: 4.0.2
|
cross-spawn: 4.0.2
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -23033,7 +23039,7 @@ snapshots:
|
||||||
|
|
||||||
pdf-parse@1.1.1:
|
pdf-parse@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
node-ensure: 0.0.0
|
node-ensure: 0.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -23862,7 +23868,7 @@ snapshots:
|
||||||
|
|
||||||
rhea@1.0.24:
|
rhea@1.0.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue