From c8d604d2c466dd84ec24f4f092183d86e43f2518 Mon Sep 17 00:00:00 2001 From: mfsiega <93014743+mfsiega@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:51:40 +0100 Subject: [PATCH] Merge commit from fork * enforce content type multipart/form-data when we handle body.files * fix unit tests --- .../nodes/trigger/ChatTrigger/ChatTrigger.node.ts | 2 ++ .../nodes-base/nodes/Form/test/Form.node.test.ts | 5 ++++- .../nodes/Form/test/FormTriggerV2.node.test.ts | 12 ++++++++++-- packages/nodes-base/nodes/Form/test/utils.test.ts | 11 +++++++++-- packages/nodes-base/nodes/Form/utils/utils.ts | 3 +++ packages/nodes-base/nodes/Webhook/Webhook.node.ts | 2 ++ .../nodes-base/utils/sendAndWait/test/util.test.ts | 1 + 7 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts index 3f6a075a829..d2700efeebe 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts @@ -18,6 +18,7 @@ import type { IBinaryData, INodeProperties, } from 'n8n-workflow'; +import * as a from 'node:assert'; import { cssVariables } from './constants'; import { validateAuth } from './GenericFunctions'; @@ -566,6 +567,7 @@ export class ChatTrigger extends Node { private async handleFormData(context: IWebhookFunctions) { const req = context.getRequestObject() as MultiPartFormData.Request; + a.ok(req.contentType === 'multipart/form-data', 'Expected multipart/form-data'); const options = context.getNodeParameter('options', {}) as IDataObject; const { data, files } = req.body; diff --git a/packages/nodes-base/nodes/Form/test/Form.node.test.ts b/packages/nodes-base/nodes/Form/test/Form.node.test.ts index 7739cbe9ed0..4657d7b6ce9 100644 --- a/packages/nodes-base/nodes/Form/test/Form.node.test.ts +++ b/packages/nodes-base/nodes/Form/test/Form.node.test.ts @@ -172,7 +172,10 @@ describe('Form Node', () => { }); it('should return form data for POST request', async () => { - mockWebhookFunctions.getRequestObject.mockReturnValue({ method: 'POST' } as Request); + mockWebhookFunctions.getRequestObject.mockReturnValue({ + method: 'POST', + contentType: 'multipart/form-data', + } as Request); mockWebhookFunctions.getParentNodes.mockReturnValue([ { type: 'n8n-nodes-base.formTrigger', diff --git a/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts b/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts index 8bf0cfdacf4..de6a57318d5 100644 --- a/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts +++ b/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts @@ -148,7 +148,11 @@ describe('FormTrigger', () => { formFields: { values: formFields }, }, }, - request: { method: 'POST' }, + request: { + method: 'POST', + headers: { 'content-type': 'multipart/form-data' }, + contentType: 'multipart/form-data', + }, bodyData, }); @@ -317,7 +321,11 @@ describe('FormTrigger', () => { formFields: { values: formFields }, }, }, - request: { method: 'POST' }, + request: { + method: 'POST', + headers: { 'content-type': 'multipart/form-data' }, + contentType: 'multipart/form-data', + }, bodyData, }); diff --git a/packages/nodes-base/nodes/Form/test/utils.test.ts b/packages/nodes-base/nodes/Form/test/utils.test.ts index 313afcf72c4..7ad42667e51 100644 --- a/packages/nodes-base/nodes/Form/test/utils.test.ts +++ b/packages/nodes-base/nodes/Form/test/utils.test.ts @@ -542,7 +542,10 @@ describe('FormTrigger, formWebhook', () => { executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields); executeFunctions.getResponseObject.mockReturnValue({ status: mockStatus, end: mockEnd } as any); - executeFunctions.getRequestObject.mockReturnValue({ method: 'POST' } as any); + executeFunctions.getRequestObject.mockReturnValue({ + method: 'POST', + contentType: 'multipart/form-data', + } as any); executeFunctions.getBodyData.mockReturnValue({ data: bodyData, files: {} }); const result = await formWebhook(executeFunctions); @@ -1310,7 +1313,9 @@ jest.mock('luxon', () => ({ describe('prepareFormReturnItem', () => { const mockContext = mock({ - getRequestObject: jest.fn().mockReturnValue({ method: 'GET', query: {} }), + getRequestObject: jest + .fn() + .mockReturnValue({ method: 'GET', query: {}, contentType: 'multipart/form-data' }), nodeHelpers: mock({ copyBinaryFile: jest.fn().mockResolvedValue({}), }), @@ -1456,6 +1461,7 @@ describe('prepareFormReturnItem', () => { mockContext.getRequestObject.mockReturnValue({ method: 'POST', query: { param: 'value' }, + contentType: 'multipart/form-data', } as unknown as Request); const result = await prepareFormReturnItem(mockContext, [], 'test'); @@ -1467,6 +1473,7 @@ describe('prepareFormReturnItem', () => { mockContext.getRequestObject.mockReturnValue({ method: 'POST', query: {}, + contentType: 'multipart/form-data', } as unknown as Request); const result = await prepareFormReturnItem(mockContext, [], 'test'); diff --git a/packages/nodes-base/nodes/Form/utils/utils.ts b/packages/nodes-base/nodes/Form/utils/utils.ts index 95accf4ff56..b7b70d0a8bf 100644 --- a/packages/nodes-base/nodes/Form/utils/utils.ts +++ b/packages/nodes-base/nodes/Form/utils/utils.ts @@ -1,5 +1,6 @@ import type { Response } from 'express'; import isbot from 'isbot'; +import * as a from 'node:assert'; import { DateTime } from 'luxon'; import type { INodeExecutionData, @@ -352,6 +353,8 @@ export async function prepareFormReturnItem( mode: 'test' | 'production', useWorkflowTimezone: boolean = false, ) { + const req = context.getRequestObject() as MultiPartFormData.Request; + a.ok(req.contentType === 'multipart/form-data', 'Expected multipart/form-data'); const bodyData = (context.getBodyData().data as IDataObject) ?? {}; const files = (context.getBodyData().files as IDataObject) ?? {}; diff --git a/packages/nodes-base/nodes/Webhook/Webhook.node.ts b/packages/nodes-base/nodes/Webhook/Webhook.node.ts index 1f5040a4b2d..31679ec0834 100644 --- a/packages/nodes-base/nodes/Webhook/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook/Webhook.node.ts @@ -12,6 +12,7 @@ import type { INodeProperties, } from 'n8n-workflow'; import { BINARY_ENCODING, NodeOperationError, Node } from 'n8n-workflow'; +import * as a from 'node:assert'; import { pipeline } from 'stream/promises'; import { file as tmpFile } from 'tmp-promise'; import { v4 as uuid } from 'uuid'; @@ -316,6 +317,7 @@ export class Webhook extends Node { prepareOutput: (data: INodeExecutionData) => INodeExecutionData[][], ) { const req = context.getRequestObject() as MultiPartFormData.Request; + a.ok(req.contentType === 'multipart/form-data', 'Expected multipart/form-data'); const options = context.getNodeParameter('options', {}) as IDataObject; const { data, files } = req.body; diff --git a/packages/nodes-base/utils/sendAndWait/test/util.test.ts b/packages/nodes-base/utils/sendAndWait/test/util.test.ts index fd9feab08eb..13ebb73f63e 100644 --- a/packages/nodes-base/utils/sendAndWait/test/util.test.ts +++ b/packages/nodes-base/utils/sendAndWait/test/util.test.ts @@ -330,6 +330,7 @@ describe('Send and Wait utils tests', () => { it('should handle customForm POST webhook', async () => { mockWebhookFunctions.getRequestObject.mockReturnValue({ method: 'POST', + contentType: 'multipart/form-data', } as any); mockWebhookFunctions.getNode.mockReturnValue({} as any);