fix(HTTP Request Node): Trim url whitespace (#27381)

Co-authored-by: yehorkardash <yehor.kardash@n8n.io>
This commit is contained in:
Khadyot Takale 2026-05-19 13:22:14 +05:30 committed by GitHub
parent 21a3090152
commit 47ffd1cc07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 234 additions and 18 deletions

View File

@ -693,7 +693,7 @@ export class HttpRequestV1 implements INodeType {
const parametersAreJson = this.getNodeParameter('jsonParameters', itemIndex);
const options = this.getNodeParameter('options', itemIndex, {});
const url = this.getNodeParameter('url', itemIndex);
let url = this.getNodeParameter('url', itemIndex);
if (typeof url !== 'string') {
const actualType = url === null ? 'null' : typeof url;
@ -703,6 +703,12 @@ export class HttpRequestV1 implements INodeType {
);
}
url = url.trim();
if (!url) {
throw new NodeOperationError(this.getNode(), 'URL parameter cannot be empty');
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new NodeOperationError(
this.getNode(),

View File

@ -740,7 +740,7 @@ export class HttpRequestV2 implements INodeType {
const parametersAreJson = this.getNodeParameter('jsonParameters', itemIndex);
const options = this.getNodeParameter('options', itemIndex, {});
const url = this.getNodeParameter('url', itemIndex);
let url = this.getNodeParameter('url', itemIndex);
if (typeof url !== 'string') {
const actualType = url === null ? 'null' : typeof url;
@ -750,6 +750,12 @@ export class HttpRequestV2 implements INodeType {
);
}
url = url.trim();
if (!url) {
throw new NodeOperationError(this.getNode(), 'URL parameter cannot be empty');
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new NodeOperationError(
this.getNode(),

View File

@ -218,7 +218,7 @@ export class HttpRequestV3 implements INodeType {
}
}
const url = this.getNodeParameter('url', itemIndex);
let url = this.getNodeParameter('url', itemIndex);
if (typeof url !== 'string') {
const actualType = url === null ? 'null' : typeof url;
@ -228,6 +228,12 @@ export class HttpRequestV3 implements INodeType {
);
}
url = url.trim();
if (!url) {
throw new NodeOperationError(this.getNode(), 'URL parameter cannot be empty');
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new NodeOperationError(
this.getNode(),

View File

@ -15,21 +15,37 @@ describe('HttpRequestV1', () => {
};
node = new HttpRequestV1(baseDescription);
executeFunctions = {
getInputData: jest.fn(() => [{ json: {} }]),
getInputData: jest.fn(),
getNodeParameter: jest.fn(),
getNode: jest.fn(() => ({
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
})),
getNode: jest.fn(() => {
return {
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
};
}),
getCredentials: jest.fn(),
helpers: {
request: jest.fn(),
requestOAuth1: jest.fn(),
requestOAuth2: jest.fn(),
requestOAuth1: jest.fn(
async () =>
await Promise.resolve({
success: true,
}),
),
requestOAuth2: jest.fn(
async () =>
await Promise.resolve({
success: true,
}),
),
requestWithAuthentication: jest.fn(),
requestWithAuthenticationPaginated: jest.fn(),
assertBinaryData: jest.fn(),
getBinaryStream: jest.fn(),
getBinaryMetadata: jest.fn(),
binaryToString: jest.fn(),
binaryToString: jest.fn((buffer: Buffer) => {
return buffer.toString();
}),
prepareBinaryData: jest.fn(),
},
getContext: jest.fn(),
@ -40,27 +56,93 @@ describe('HttpRequestV1', () => {
});
describe('URL Parameter Validation', () => {
it.each([
{ url: undefined, expectedType: 'undefined' },
{ url: null, expectedType: 'null' },
{ url: 42, expectedType: 'number' },
])('should throw error when URL is $expectedType', async ({ url, expectedType }) => {
it('should throw error when URL is only whitespace', async () => {
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'responseFormat':
return 'json';
case 'requestMethod':
return 'GET';
case 'url':
return ' ';
case 'jsonParameters':
return false;
case 'options':
return {};
case 'url':
return url;
default:
return undefined;
}
});
(executeFunctions.getCredentials as jest.Mock).mockRejectedValue(new Error('No credentials'));
await expect(node.execute.call(executeFunctions)).rejects.toThrow(
'URL parameter cannot be empty',
);
});
it('should trim whitespace from valid URL', async () => {
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'responseFormat':
return 'json';
case 'requestMethod':
return 'GET';
case 'url':
return ' http://example.com ';
case 'jsonParameters':
return false;
case 'options':
return {};
case 'bodyParametersUi':
case 'headerParametersUi':
case 'queryParametersUi':
return { parameter: [] };
default:
return undefined;
}
});
(executeFunctions.getCredentials as jest.Mock).mockRejectedValue(new Error('No credentials'));
const response = {
success: true,
};
(executeFunctions.helpers.request as jest.Mock).mockResolvedValue(response);
const result = await node.execute.call(executeFunctions);
expect(result).toEqual([[{ json: { success: true }, pairedItem: { item: 0 } }]]);
expect(executeFunctions.helpers.request).toHaveBeenCalledTimes(1);
const requestArgs = (executeFunctions.helpers.request as jest.Mock).mock.calls[0][0];
expect(requestArgs.uri ?? requestArgs.url).toBe('http://example.com');
});
it.each([
{ url: undefined, expectedType: 'undefined' },
{ url: null, expectedType: 'null' },
{ url: 42, expectedType: 'number' },
])('should throw error when URL is $expectedType', async ({ url, expectedType }) => {
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'responseFormat':
return 'json';
case 'requestMethod':
return 'GET';
case 'url':
return url;
case 'jsonParameters':
return false;
case 'options':
return {};
case 'bodyParametersUi':
case 'headerParametersUi':
case 'queryParametersUi':
return { parameter: [] };
default:
return undefined;
}
});
(executeFunctions.getCredentials as jest.Mock).mockRejectedValue(new Error('No credentials'));
await expect(node.execute.call(executeFunctions)).rejects.toThrow(
`URL parameter must be a string, got ${expectedType}`,

View File

@ -162,6 +162,68 @@ describe('HttpRequestV2', () => {
});
describe('URL Parameter Validation', () => {
it('should throw error when URL is only whitespace', async () => {
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'responseFormat':
return 'json';
case 'requestMethod':
return 'GET';
case 'url':
return ' ';
case 'authentication':
return 'none';
case 'jsonParameters':
return false;
case 'options':
return options;
default:
return undefined;
}
});
await expect(node.execute.call(executeFunctions)).rejects.toThrow(
'URL parameter cannot be empty',
);
});
it('should trim whitespace from valid URL', async () => {
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'responseFormat':
return 'json';
case 'requestMethod':
return 'GET';
case 'url':
return ' http://example.com ';
case 'authentication':
return 'none';
case 'jsonParameters':
return false;
case 'options':
return options;
case 'bodyParametersUi':
case 'headerParametersUi':
case 'queryParametersUi':
return { parameter: [] };
default:
return undefined;
}
});
const response = {
success: true,
};
(executeFunctions.helpers.request as jest.Mock).mockResolvedValue(response);
const result = await node.execute.call(executeFunctions);
expect(result).toEqual([[{ json: { success: true }, pairedItem: { item: 0 } }]]);
expect(executeFunctions.helpers.request).toHaveBeenCalledTimes(1);
const requestArgs = (executeFunctions.helpers.request as jest.Mock).mock.calls[0][0];
expect(requestArgs.uri ?? requestArgs.url).toBe('http://example.com');
});
it.each([
{ url: undefined, expectedType: 'undefined' },
{ url: null, expectedType: 'null' },
@ -182,6 +244,10 @@ describe('HttpRequestV2', () => {
return false;
case 'options':
return options;
case 'bodyParametersUi':
case 'headerParametersUi':
case 'queryParametersUi':
return { parameter: [] };
default:
return undefined;
}

View File

@ -298,6 +298,56 @@ describe('HttpRequestV3', () => {
'URL parameter must be a string, got number',
);
});
it('should throw error when URL is only whitespace', async () => {
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return ' ';
case 'authentication':
return 'none';
case 'options':
return options;
default:
return undefined;
}
});
await expect(node.execute.call(executeFunctions)).rejects.toThrow(
'URL parameter cannot be empty',
);
});
it('should trim whitespace from valid URL', async () => {
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: {} }]);
(executeFunctions.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return ' http://example.com ';
case 'authentication':
return 'none';
case 'options':
return options;
default:
return undefined;
}
});
const response = {
headers: { 'content-type': 'application/json' },
body: Buffer.from(JSON.stringify({ success: true })),
};
(executeFunctions.helpers.request as jest.Mock).mockResolvedValue(response);
const result = await node.execute.call(executeFunctions);
expect(result).toEqual([[{ json: { success: true }, pairedItem: { item: 0 } }]]);
expect(executeFunctions.helpers.request).toHaveBeenCalledTimes(1);
const requestArgs = (executeFunctions.helpers.request as jest.Mock).mock.calls[0][0];
expect(requestArgs.uri ?? requestArgs.url).toBe('http://example.com');
});
});
describe('JSON Parameter Validation', () => {