diff --git a/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts b/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts index c4824dcd058..7dde07468f7 100644 --- a/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts @@ -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(), diff --git a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts index 2f7f5b75647..470e65f8511 100644 --- a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts @@ -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(), diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index 67ccfa831c7..134e8534767 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -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(), diff --git a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV1.test.ts b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV1.test.ts index 24163a65a88..a09fbbe35d9 100644 --- a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV1.test.ts +++ b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV1.test.ts @@ -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}`, diff --git a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts index 2dffa30a03f..0de3fff2094 100644 --- a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts +++ b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV2.test.ts @@ -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; } diff --git a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts index 115332e4b2e..f955c810bae 100644 --- a/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts +++ b/packages/nodes-base/nodes/HttpRequest/test/node/HttpRequestV3.test.ts @@ -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', () => {