From 91a827a4ec2c4b6ea0051f6727bddfdcf2ee8d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 24 Feb 2025 18:10:47 +0100 Subject: [PATCH] try to unify webhook response modes --- .../webhooks/__tests__/waiting-forms.test.ts | 4 +- .../__tests__/waiting-webhooks.test.ts | 4 +- .../__tests__/webhook-helpers.test.ts | 35 +- .../__tests__/webhook-request-handler.test.ts | 30 +- packages/cli/src/webhooks/live-webhooks.ts | 50 +- packages/cli/src/webhooks/test-webhooks.ts | 101 ++- packages/cli/src/webhooks/waiting-forms.ts | 14 +- packages/cli/src/webhooks/waiting-webhooks.ts | 58 +- packages/cli/src/webhooks/webhook-helpers.ts | 645 ++++++++---------- .../src/webhooks/webhook-request-handler.ts | 13 +- packages/cli/src/webhooks/webhook.types.ts | 16 +- .../cli/test/integration/webhooks.test.ts | 13 - .../RespondToWebhook/RespondToWebhook.node.ts | 29 +- packages/workflow/src/Interfaces.ts | 4 +- 14 files changed, 463 insertions(+), 553 deletions(-) diff --git a/packages/cli/src/webhooks/__tests__/waiting-forms.test.ts b/packages/cli/src/webhooks/__tests__/waiting-forms.test.ts index 0ae8d5f7d8..4dbcfae297 100644 --- a/packages/cli/src/webhooks/__tests__/waiting-forms.test.ts +++ b/packages/cli/src/webhooks/__tests__/waiting-forms.test.ts @@ -6,7 +6,7 @@ import type { ExecutionRepository } from '@/databases/repositories/execution.rep import { WaitingForms } from '@/webhooks/waiting-forms'; import type { IExecutionResponse } from '../../interfaces'; -import type { WaitingWebhookRequest } from '../webhook.types'; +import type { IWebhookResponsePromiseData, WaitingWebhookRequest } from '../webhook.types'; describe('WaitingForms', () => { const executionRepository = mock(); @@ -205,7 +205,7 @@ describe('WaitingForms', () => { // @ts-expect-error Protected method .spyOn(waitingForms, 'getWebhookExecutionData') // @ts-expect-error Protected method - .mockResolvedValue(mock()); + .mockResolvedValue(mock()); const execution = mock({ finished: false, diff --git a/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts b/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts index a752b46795..800100589f 100644 --- a/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts +++ b/packages/cli/src/webhooks/__tests__/waiting-webhooks.test.ts @@ -6,7 +6,7 @@ import { ConflictError } from '@/errors/response-errors/conflict.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { IExecutionResponse } from '@/interfaces'; import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; -import type { IWebhookResponseCallbackData, WaitingWebhookRequest } from '@/webhooks/webhook.types'; +import type { IWebhookResponsePromiseData, WaitingWebhookRequest } from '@/webhooks/webhook.types'; describe('WaitingWebhooks', () => { const executionRepository = mock(); @@ -85,7 +85,7 @@ describe('WaitingWebhooks', () => { // @ts-expect-error Protected method .spyOn(waitingWebhooks, 'getWebhookExecutionData') // @ts-expect-error Protected method - .mockResolvedValue(mock()); + .mockResolvedValue(mock()); const execution = mock({ finished: false, diff --git a/packages/cli/src/webhooks/__tests__/webhook-helpers.test.ts b/packages/cli/src/webhooks/__tests__/webhook-helpers.test.ts index e383f1a7a0..ce1f03769a 100644 --- a/packages/cli/src/webhooks/__tests__/webhook-helpers.test.ts +++ b/packages/cli/src/webhooks/__tests__/webhook-helpers.test.ts @@ -1,9 +1,8 @@ import { mock, type MockProxy } from 'jest-mock-extended'; -import type { Workflow, INode, IDataObject } from 'n8n-workflow'; +import type { Workflow, INode, IN8nHttpFullResponse } from 'n8n-workflow'; import { FORM_NODE_TYPE, WAIT_NODE_TYPE } from 'n8n-workflow'; import { autoDetectResponseMode, handleFormRedirectionCase } from '../webhook-helpers'; -import type { IWebhookResponseCallbackData } from '../webhook.types'; describe('autoDetectResponseMode', () => { let workflow: MockProxy; @@ -58,40 +57,46 @@ describe('autoDetectResponseMode', () => { describe('handleFormRedirectionCase', () => { test('should return data unchanged if start node is WAIT_NODE_TYPE with resume not equal to form', () => { - const data: IWebhookResponseCallbackData = { - responseCode: 302, + const response: IN8nHttpFullResponse = { + statusCode: 302, headers: { location: 'http://example.com' }, + body: {}, }; const workflowStartNode = mock({ type: WAIT_NODE_TYPE, parameters: { resume: 'webhook' }, }); - const result = handleFormRedirectionCase(data, workflowStartNode); - expect(result).toEqual(data); + const result = handleFormRedirectionCase(response, workflowStartNode); + expect(result).toEqual(response); }); test('should modify data if start node type matches and responseCode is a redirect', () => { - const data: IWebhookResponseCallbackData = { - responseCode: 302, + const response: IN8nHttpFullResponse = { + statusCode: 302, headers: { location: 'http://example.com' }, + body: {}, }; const workflowStartNode = mock({ type: FORM_NODE_TYPE, parameters: {}, }); - const result = handleFormRedirectionCase(data, workflowStartNode); - expect(result.responseCode).toBe(200); - expect(result.data).toEqual({ redirectURL: 'http://example.com' }); - expect((result?.headers as IDataObject)?.location).toBeUndefined(); + const result = handleFormRedirectionCase(response, workflowStartNode); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual({ redirectURL: 'http://example.com' }); + expect(result.headers.location).toBeUndefined(); }); test('should not modify data if location header is missing', () => { - const data: IWebhookResponseCallbackData = { responseCode: 302, headers: {} }; + const response: IN8nHttpFullResponse = { + statusCode: 302, + headers: {}, + body: {}, + }; const workflowStartNode = mock({ type: FORM_NODE_TYPE, parameters: {}, }); - const result = handleFormRedirectionCase(data, workflowStartNode); - expect(result).toEqual(data); + const result = handleFormRedirectionCase(response, workflowStartNode); + expect(result).toEqual(response); }); }); diff --git a/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts b/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts index f740a9c726..052b1a6761 100644 --- a/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts +++ b/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts @@ -7,7 +7,7 @@ import { ResponseError } from '@/errors/response-errors/abstract/response.error' import { createWebhookHandlerFor } from '@/webhooks/webhook-request-handler'; import type { IWebhookManager, - IWebhookResponseCallbackData, + IWebhookResponsePromiseData, WebhookOptionsRequest, WebhookRequest, } from '@/webhooks/webhook.types'; @@ -150,14 +150,16 @@ describe('WebhookRequestHandler', () => { const res = mock(); - const executeWebhookResponse: IWebhookResponseCallbackData = { - responseCode: 200, - data: {}, - headers: { - 'x-custom-header': 'test', + const executeWebhookResponse: IWebhookResponsePromiseData = { + response: { + statusCode: 200, + body: {}, + headers: { + 'x-custom-header': 'test', + }, }, }; - webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse); + // webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse); await handler(req, res); @@ -166,7 +168,7 @@ describe('WebhookRequestHandler', () => { expect(res.header).toHaveBeenCalledWith({ 'x-custom-header': 'test', }); - expect(res.json).toHaveBeenCalledWith(executeWebhookResponse.data); + expect(res.json).toHaveBeenCalledWith(executeWebhookResponse.response.body); }); it('should send an error response if webhook execution throws', async () => { @@ -204,16 +206,20 @@ describe('WebhookRequestHandler', () => { const res = mock(); - const executeWebhookResponse: IWebhookResponseCallbackData = { - responseCode: 200, + const executeWebhookResponse: IWebhookResponsePromiseData = { + response: { + statusCode: 200, + headers: {}, + body: {}, + }, }; - webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse); + // webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse); await handler(req, res); expect(webhookManager.executeWebhook).toHaveBeenCalledWith(req, res); expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith(executeWebhookResponse.data); + expect(res.json).toHaveBeenCalledWith(executeWebhookResponse.response.body); }, ); }); diff --git a/packages/cli/src/webhooks/live-webhooks.ts b/packages/cli/src/webhooks/live-webhooks.ts index 440e8254dc..f87a6a17ae 100644 --- a/packages/cli/src/webhooks/live-webhooks.ts +++ b/packages/cli/src/webhooks/live-webhooks.ts @@ -1,7 +1,7 @@ import { Service } from '@n8n/di'; import type { Response } from 'express'; import { Logger } from 'n8n-core'; -import { Workflow, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow'; +import { Workflow, CHAT_TRIGGER_NODE_TYPE, createDeferredPromise } from 'n8n-workflow'; import type { INode, IWebhookData, IHttpRequestMethods } from 'n8n-workflow'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; @@ -14,7 +14,7 @@ import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-da import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service'; import type { - IWebhookResponseCallbackData, + IWebhookResponsePromiseData, IWebhookManager, WebhookAccessControlOptions, WebhookRequest, @@ -66,10 +66,7 @@ export class LiveWebhooks implements IWebhookManager { /** * Checks if a webhook for the given method and path exists and executes the workflow. */ - async executeWebhook( - request: WebhookRequest, - response: Response, - ): Promise { + async executeWebhook(request: WebhookRequest, response: Response): Promise { const httpMethod = request.method; const path = request.params.path; @@ -126,29 +123,24 @@ export class LiveWebhooks implements IWebhookManager { throw new NotFoundError('Could not find node to process webhook.'); } - return await new Promise((resolve, reject) => { - const executionMode = 'webhook'; - void WebhookHelpers.executeWebhook( - workflow, - webhookData, - workflowData, - workflowStartNode, - executionMode, - undefined, - undefined, - undefined, - request, - response, - async (error: Error | null, data: object) => { - if (error !== null) { - return reject(error); - } - // Save static data if it changed - await this.workflowStaticDataService.saveStaticData(workflow); - resolve(data); - }, - ); - }); + const executionMode = 'webhook'; + const responsePromise = createDeferredPromise(); + await WebhookHelpers.executeWebhook( + workflow, + webhookData, + workflowData, + workflowStartNode, + executionMode, + undefined, + undefined, + undefined, + request, + response, + responsePromise, + ); + + // Save static data if it changed + await this.workflowStaticDataService.saveStaticData(workflow); } private async findWebhook(path: string, httpMethod: IHttpRequestMethods) { diff --git a/packages/cli/src/webhooks/test-webhooks.ts b/packages/cli/src/webhooks/test-webhooks.ts index f944021e47..92e71ec8a1 100644 --- a/packages/cli/src/webhooks/test-webhooks.ts +++ b/packages/cli/src/webhooks/test-webhooks.ts @@ -1,7 +1,7 @@ import { Service } from '@n8n/di'; import type express from 'express'; import { InstanceSettings } from 'n8n-core'; -import { WebhookPathTakenError, Workflow } from 'n8n-workflow'; +import { createDeferredPromise, WebhookPathTakenError, Workflow } from 'n8n-workflow'; import type { IWebhookData, IWorkflowExecuteAdditionalData, @@ -26,7 +26,7 @@ import type { WorkflowRequest } from '@/workflows/workflow.request'; import { WebhookService } from './webhook.service'; import type { - IWebhookResponseCallbackData, + IWebhookResponsePromiseData, IWebhookManager, WebhookAccessControlOptions, WebhookRequest, @@ -53,10 +53,7 @@ export class TestWebhooks implements IWebhookManager { * Return a promise that resolves when the test webhook is called. * Also inform the FE of the result and remove the test webhook. */ - async executeWebhook( - request: WebhookRequest, - response: express.Response, - ): Promise { + async executeWebhook(request: WebhookRequest, response: express.Response): Promise { const httpMethod = request.method; let path = removeTrailingSlash(request.params.path); @@ -113,59 +110,55 @@ export class TestWebhooks implements IWebhookManager { throw new NotFoundError('Could not find node to process webhook.'); } - return await new Promise(async (resolve, reject) => { - try { - const executionMode = 'manual'; - const executionId = await WebhookHelpers.executeWebhook( - workflow, - webhook, - workflowEntity, - workflowStartNode, - executionMode, + const executionMode = 'manual'; + const responsePromise = createDeferredPromise(); + try { + const executionId = await WebhookHelpers.executeWebhook( + workflow, + webhook, + workflowEntity, + workflowStartNode, + executionMode, + pushRef, + undefined, // IRunExecutionData + undefined, // executionId + request, + response, + responsePromise, + destinationNode, + ); + + // The workflow did not run as the request was probably setup related + // or a ping so do not resolve the promise and wait for the real webhook + // request instead. + if (executionId === undefined) return; + + // Inform editor-ui that webhook got received + if (pushRef !== undefined) { + this.push.send( + { type: 'testWebhookReceived', data: { workflowId: webhook?.workflowId, executionId } }, pushRef, - undefined, // IRunExecutionData - undefined, // executionId - request, - response, - (error: Error | null, data: IWebhookResponseCallbackData) => { - if (error !== null) reject(error); - else resolve(data); - }, - destinationNode, ); - - // The workflow did not run as the request was probably setup related - // or a ping so do not resolve the promise and wait for the real webhook - // request instead. - if (executionId === undefined) return; - - // Inform editor-ui that webhook got received - if (pushRef !== undefined) { - this.push.send( - { type: 'testWebhookReceived', data: { workflowId: webhook?.workflowId, executionId } }, - pushRef, - ); - } - } catch {} - - /** - * Multi-main setup: In a manual webhook execution, the main process that - * handles a webhook might not be the same as the main process that created - * the webhook. If so, after the test webhook has been successfully executed, - * the handler process commands the creator process to clear its test webhooks. - */ - if (this.instanceSettings.isMultiMain && pushRef && !this.push.hasPushRef(pushRef)) { - void this.publisher.publishCommand({ - command: 'clear-test-webhooks', - payload: { webhookKey: key, workflowEntity, pushRef }, - }); - return; } + } catch {} - this.clearTimeout(key); + /** + * Multi-main setup: In a manual webhook execution, the main process that + * handles a webhook might not be the same as the main process that created + * the webhook. If so, after the test webhook has been successfully executed, + * the handler process commands the creator process to clear its test webhooks. + */ + if (this.instanceSettings.isMultiMain && pushRef && !this.push.hasPushRef(pushRef)) { + void this.publisher.publishCommand({ + command: 'clear-test-webhooks', + payload: { webhookKey: key, workflowEntity, pushRef }, + }); + return; + } - await this.deactivateWebhooks(workflow); - }); + this.clearTimeout(key); + + await this.deactivateWebhooks(workflow); } clearTimeout(key: string) { diff --git a/packages/cli/src/webhooks/waiting-forms.ts b/packages/cli/src/webhooks/waiting-forms.ts index 2c8a65afe0..4ea4699607 100644 --- a/packages/cli/src/webhooks/waiting-forms.ts +++ b/packages/cli/src/webhooks/waiting-forms.ts @@ -8,7 +8,7 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { IExecutionResponse } from '@/interfaces'; import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; -import type { IWebhookResponseCallbackData, WaitingWebhookRequest } from './webhook.types'; +import type { WaitingWebhookRequest } from './webhook.types'; @Service() export class WaitingForms extends WaitingWebhooks { @@ -61,10 +61,7 @@ export class WaitingForms extends WaitingWebhooks { } } - async executeWebhook( - req: WaitingWebhookRequest, - res: express.Response, - ): Promise { + async executeWebhook(req: WaitingWebhookRequest, res: express.Response): Promise { const { path: executionId, suffix } = req.params; this.logReceivedWebhook(req.method, executionId); @@ -107,10 +104,7 @@ export class WaitingForms extends WaitingWebhooks { message: 'Your response has been recorded', formTitle: 'Form Submitted', }); - - return { - noWebhookResponse: true, - }; + return; } else { lastNodeExecuted = completionPage; } @@ -122,7 +116,7 @@ export class WaitingForms extends WaitingWebhooks { */ if (execution.mode === 'manual') execution.data.isTestWebhook = true; - return await this.getWebhookExecutionData({ + await this.getWebhookExecutionData({ execution, req, res, diff --git a/packages/cli/src/webhooks/waiting-webhooks.ts b/packages/cli/src/webhooks/waiting-webhooks.ts index 585a39a18c..5993a21659 100644 --- a/packages/cli/src/webhooks/waiting-webhooks.ts +++ b/packages/cli/src/webhooks/waiting-webhooks.ts @@ -1,10 +1,10 @@ import { Service } from '@n8n/di'; import type express from 'express'; import { Logger } from 'n8n-core'; +import type { INodes, IWorkflowBase } from 'n8n-workflow'; import { + createDeferredPromise, FORM_NODE_TYPE, - type INodes, - type IWorkflowBase, SEND_AND_WAIT_OPERATION, WAIT_NODE_TYPE, Workflow, @@ -20,7 +20,7 @@ import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-da import { WebhookService } from './webhook.service'; import type { - IWebhookResponseCallbackData, + IWebhookResponsePromiseData, IWebhookManager, WaitingWebhookRequest, } from './webhook.types'; @@ -81,10 +81,7 @@ export class WaitingWebhooks implements IWebhookManager { }); } - async executeWebhook( - req: WaitingWebhookRequest, - res: express.Response, - ): Promise { + async executeWebhook(req: WaitingWebhookRequest, res: express.Response): Promise { const { path: executionId, suffix } = req.params; this.logReceivedWebhook(req.method, executionId); @@ -113,7 +110,7 @@ export class WaitingWebhooks implements IWebhookManager { const { nodes } = this.createWorkflow(workflowData); if (this.isSendAndWaitRequest(nodes, suffix)) { res.render('send-and-wait-no-action-required', { isTestWebhook: false }); - return { noWebhookResponse: true }; + return; } else { throw new ConflictError(`The execution "${executionId} has finished already.`); } @@ -127,7 +124,7 @@ export class WaitingWebhooks implements IWebhookManager { */ if (execution.mode === 'manual') execution.data.isTestWebhook = true; - return await this.getWebhookExecutionData({ + await this.getWebhookExecutionData({ execution, req, res, @@ -151,7 +148,7 @@ export class WaitingWebhooks implements IWebhookManager { lastNodeExecuted: string; executionId: string; suffix?: string; - }): Promise { + }): Promise { // Set the node as disabled so that the data does not get executed again as it would result // in starting the wait all over again this.disableNode(execution, req.method); @@ -188,7 +185,7 @@ export class WaitingWebhooks implements IWebhookManager { if (this.isSendAndWaitRequest(workflow.nodes, suffix)) { res.render('send-and-wait-no-action-required', { isTestWebhook: false }); - return { noWebhookResponse: true }; + return; } if (!execution.data.resultData.error && execution.status === 'waiting') { @@ -203,7 +200,7 @@ export class WaitingWebhooks implements IWebhookManager { ); if (hasChildForms) { - return { noWebhookResponse: true }; + return; } } @@ -212,27 +209,20 @@ export class WaitingWebhooks implements IWebhookManager { const runExecutionData = execution.data; - return await new Promise((resolve, reject) => { - const executionMode = 'webhook'; - void WebhookHelpers.executeWebhook( - workflow, - webhookData, - workflowData, - workflowStartNode, - executionMode, - runExecutionData.pushRef, - runExecutionData, - execution.id, - req, - res, - - (error: Error | null, data: object) => { - if (error !== null) { - return reject(error); - } - resolve(data); - }, - ); - }); + const executionMode = 'webhook'; + const responsePromise = createDeferredPromise(); + await WebhookHelpers.executeWebhook( + workflow, + webhookData, + workflowData, + workflowStartNode, + executionMode, + runExecutionData.pushRef, + runExecutionData, + execution.id, + req, + res, + responsePromise, + ); } } diff --git a/packages/cli/src/webhooks/webhook-helpers.ts b/packages/cli/src/webhooks/webhook-helpers.ts index 90ec615e24..1c34c7d27d 100644 --- a/packages/cli/src/webhooks/webhook-helpers.ts +++ b/packages/cli/src/webhooks/webhook-helpers.ts @@ -34,7 +34,6 @@ import type { import { ApplicationError, BINARY_ENCODING, - createDeferredPromise, ExecutionCancelledError, FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, @@ -60,7 +59,7 @@ import * as WorkflowHelpers from '@/workflow-helpers'; import { WorkflowRunner } from '@/workflow-runner'; import { WebhookService } from './webhook.service'; -import type { IWebhookResponseCallbackData, WebhookRequest } from './webhook.types'; +import type { IWebhookResponsePromiseData, WebhookRequest } from './webhook.types'; /** * Returns all the webhooks which should be created for the given workflow @@ -107,7 +106,7 @@ export function autoDetectResponseMode( workflowStartNode: INode, workflow: Workflow, method: string, -) { +): WebhookResponseMode | undefined { if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') { return undefined; } @@ -137,26 +136,25 @@ export function autoDetectResponseMode( * for formTrigger and form nodes redirection has to be handled by sending redirectURL in response body */ export const handleFormRedirectionCase = ( - data: IWebhookResponseCallbackData, + response: IN8nHttpFullResponse, workflowStartNode: INode, -) => { +): IN8nHttpFullResponse => { if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') { - return data; + return response; } + const { headers, statusCode } = response; if ( [FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(workflowStartNode.type) && - (data?.headers as IDataObject)?.location && - String(data?.responseCode).startsWith('3') + headers.location && + String(statusCode).startsWith('3') ) { - data.responseCode = 200; - data.data = { - redirectURL: (data?.headers as IDataObject)?.location, - }; - (data.headers as IDataObject).location = undefined; + response.statusCode = 200; + response.body = { redirectURL: headers.location }; + delete headers.location; } - return data; + return response; }; const { formDataFileSizeMax } = Container.get(GlobalConfig).endpoints; @@ -177,7 +175,7 @@ export async function executeWebhook( executionId: string | undefined, req: WebhookRequest, res: express.Response, - responseCallback: (error: Error | null, data: IWebhookResponseCallbackData) => void, + responsePromise: IDeferredPromise, destinationNode?: string, ): Promise { // Get the nodeType to know which responseMode is set @@ -185,11 +183,6 @@ export async function executeWebhook( workflowStartNode.type, workflowStartNode.typeVersion, ); - if (nodeType === undefined) { - const errorMessage = `The type of the webhook node "${workflowStartNode.name}" is not known`; - responseCallback(new ApplicationError(errorMessage), {}); - throw new InternalServerError(errorMessage); - } const additionalKeys: IWorkflowDataProxyAdditionalKeys = { $executionId: executionId, @@ -209,24 +202,28 @@ export async function executeWebhook( additionalData.executionId = executionId; } - // Get the responseMode - let responseMode; - //check if response mode should be set automatically, e.g. multipage form - responseMode = autoDetectResponseMode(workflowStartNode, workflow, req.method); - - if (!responseMode) { - responseMode = workflow.expression.getSimpleParameterValue( + const responseMode = + autoDetectResponseMode(workflowStartNode, workflow, req.method) ?? + (workflow.expression.getSimpleParameterValue( workflowStartNode, webhookData.webhookDescription.responseMode, executionMode, additionalKeys, undefined, 'onReceived', - ) as WebhookResponseMode; + ) as WebhookResponseMode); + + if (!['onReceived', 'lastNode', 'responseNode'].includes(responseMode)) { + // If the mode is not known we error. Is probably best like that instead of using + // the default that people know as early as possible (probably already testing phase) + // that something does not resolve properly. + const errorMessage = `The response mode '${responseMode}' is not valid!`; + responsePromise.reject(new ApplicationError(errorMessage)); + return; } - const responseCode = workflow.expression.getSimpleParameterValue( + const responseCodeParam = workflow.expression.getSimpleParameterValue( workflowStartNode, webhookData.webhookDescription.responseCode as string, executionMode, @@ -235,7 +232,7 @@ export async function executeWebhook( 200, ) as number; - const responseData = workflow.expression.getComplexParameterValue( + const responseDataParam = workflow.expression.getComplexParameterValue( workflowStartNode, webhookData.webhookDescription.responseData, executionMode, @@ -244,35 +241,24 @@ export async function executeWebhook( 'firstEntryJson', ); - if (!['onReceived', 'lastNode', 'responseNode'].includes(responseMode)) { - // If the mode is not known we error. Is probably best like that instead of using - // the default that people know as early as possible (probably already testing phase) - // that something does not resolve properly. - const errorMessage = `The response mode '${responseMode}' is not valid!`; - responseCallback(new ApplicationError(errorMessage), {}); - throw new InternalServerError(errorMessage); - } + const nodeVersion = workflowStartNode.typeVersion; + // binaryData option is removed in versions higher than 1 + const binaryDataParam = + nodeVersion === 1 + ? (workflow.expression.getSimpleParameterValue( + workflowStartNode, + '={{$parameter["options"]["binaryData"]}}', + executionMode, + additionalKeys, + undefined, + false, + ) as boolean) + : undefined; // Add the Response and Request so that this data can be accessed in the node additionalData.httpRequest = req; additionalData.httpResponse = res; - let binaryData; - - const nodeVersion = workflowStartNode.typeVersion; - if (nodeVersion === 1) { - // binaryData option is removed in versions higher than 1 - binaryData = workflow.expression.getSimpleParameterValue( - workflowStartNode, - '={{$parameter["options"]["binaryData"]}}', - executionMode, - additionalKeys, - undefined, - false, - ); - } - - let didSendResponse = false; let runExecutionDataMerge = {}; try { // Run the webhook function to see what should be returned and if @@ -281,7 +267,7 @@ export async function executeWebhook( // if `Webhook` or `Wait` node, and binaryData is enabled, skip pre-parse the request-body // always falsy for versions higher than 1 - if (!binaryData) { + if (!binaryDataParam) { const { contentType } = req; if (contentType === 'multipart/form-data') { req.body = await parseFormData(req); @@ -336,9 +322,6 @@ export async function executeWebhook( }, }); - responseCallback(new ApplicationError(errorMessage), {}); - didSendResponse = true; - // Add error to execution data that it can be logged and send to Editor-UI runExecutionDataMerge = { resultData: { @@ -358,16 +341,20 @@ export async function executeWebhook( // which then so gets the chance to throw the error. workflowData: [[{ json: {} }]], }; + + responsePromise.reject(new ApplicationError(errorMessage)); + return; } const additionalKeys: IWorkflowDataProxyAdditionalKeys = { $executionId: executionId, }; + const responseHeadersParam = webhookData.webhookDescription.responseHeaders; if (webhookData.webhookDescription.responseHeaders !== undefined) { const responseHeaders = workflow.expression.getComplexParameterValue( workflowStartNode, - webhookData.webhookDescription.responseHeaders, + responseHeadersParam, executionMode, additionalKeys, undefined, @@ -381,90 +368,83 @@ export async function executeWebhook( | undefined; }; - if (responseHeaders !== undefined && responseHeaders.entries !== undefined) { + if (responseHeaders?.entries?.length) { for (const item of responseHeaders.entries) { res.setHeader(item.name, item.value); } } } - if (webhookResultData.noWebhookResponse === true && !didSendResponse) { + if (webhookResultData.noWebhookResponse === true) { // The response got already send - responseCallback(null, { - noWebhookResponse: true, - }); - didSendResponse = true; + responsePromise.resolve({ noWebhookResponse: true }); + return; } if (webhookResultData.workflowData === undefined) { // Workflow should not run if (webhookResultData.webhookResponse !== undefined) { // Data to respond with is given - if (!didSendResponse) { - responseCallback(null, { - data: webhookResultData.webhookResponse, - responseCode, - }); - didSendResponse = true; - } + responsePromise.resolve({ + response: { + statusCode: responseCodeParam, + body: webhookResultData.webhookResponse, + headers: {}, + }, + }); + return; } else { // Send default response - - if (!didSendResponse) { - responseCallback(null, { - data: { - message: 'Webhook call received', - }, - responseCode, - }); - didSendResponse = true; - } + responsePromise.resolve({ + response: { + statusCode: responseCodeParam, + body: { message: 'Webhook call received' }, + headers: {}, + }, + }); + return; } return; } // Now that we know that the workflow should run we can return the default response // directly if responseMode it set to "onReceived" and a response should be sent - if (responseMode === 'onReceived' && !didSendResponse) { - // Return response directly and do not wait for the workflow to finish - if (responseData === 'noData') { - // Return without data - responseCallback(null, { - responseCode, - }); - } else if (responseData) { - // Return the data specified in the response data option - responseCallback(null, { - data: responseData as IDataObject, - responseCode, - }); - } else if (webhookResultData.webhookResponse !== undefined) { - // Data to respond with is given - responseCallback(null, { - data: webhookResultData.webhookResponse, - responseCode, - }); - } else { - responseCallback(null, { - data: { - message: 'Workflow was started', - }, - responseCode, - }); - } + if (responseMode === 'onReceived') { + void responsePromise.promise.then(async (resolveData) => { + if (resolveData.noWebhookResponse) { + // TODO: send 204 + res.end(); + return; + } + const { statusCode, headers, body } = resolveData.response; + for (const [key, value] of Object.entries(headers)) { + res.setHeader(key, value); + } + // TODO handle binary responses + res.status(statusCode).json(body); + }); - didSendResponse = true; + responsePromise.resolve({ + response: { + statusCode: responseCodeParam, + headers: {}, + body: responseDataParam ?? + webhookResultData.webhookResponse ?? { message: 'Workflow was started' }, + }, + }); + return; } // Initialize the data of the webhook node - const nodeExecutionStack: IExecuteData[] = []; - nodeExecutionStack.push({ - node: workflowStartNode, - data: { - main: webhookResultData.workflowData, + const nodeExecutionStack: IExecuteData[] = [ + { + node: workflowStartNode, + data: { + main: webhookResultData.workflowData, + }, + source: null, }, - source: null, - }); + ]; runExecutionData = runExecutionData || @@ -517,43 +497,34 @@ export async function executeWebhook( runData.pushRef = runExecutionData.pushRef; } - let responsePromise: IDeferredPromise | undefined; if (responseMode === 'responseNode') { - responsePromise = createDeferredPromise(); responsePromise.promise - .then(async (response: IN8nHttpFullResponse) => { - if (didSendResponse) { + .then(async (resolveData) => { + if (resolveData.noWebhookResponse) { return; } + const { response } = resolveData; const binaryData = (response.body as IDataObject)?.binaryData as IBinaryData; if (binaryData?.id) { res.header(response.headers); const stream = await Container.get(BinaryDataService).getAsStream(binaryData.id); stream.pipe(res, { end: false }); await finished(stream); - responseCallback(null, { noWebhookResponse: true }); + responsePromise.resolve({ noWebhookResponse: true }); } else if (Buffer.isBuffer(response.body)) { res.header(response.headers); - res.end(response.body); - responseCallback(null, { noWebhookResponse: true }); + res.write(response.body); + responsePromise.resolve({ noWebhookResponse: true }); } else { // TODO: This probably needs some more changes depending on the options on the // Webhook Response node - - let data: IWebhookResponseCallbackData = { - data: response.body as IDataObject, - headers: response.headers, - responseCode: response.statusCode, - }; - - data = handleFormRedirectionCase(data, workflowStartNode); - - responseCallback(null, data); + let data: IWebhookResponsePromiseData = { response }; + data.response = handleFormRedirectionCase(data.response, workflowStartNode); + responsePromise.resolve(data); } process.nextTick(() => res.end()); - didSendResponse = true; }) .catch(async (error) => { Container.get(ErrorReporter).error(error); @@ -561,7 +532,7 @@ export async function executeWebhook( `Error with Webhook-Response for execution "${executionId}": "${error.message}"`, { executionId, workflowId: workflow.id }, ); - responseCallback(error, {}); + responsePromise.reject(error); }); } @@ -578,7 +549,7 @@ export async function executeWebhook( executionId = await Container.get(WorkflowRunner).run( runData, true, - !didSendResponse, + true, executionId, responsePromise, ); @@ -602,221 +573,200 @@ export async function executeWebhook( }); } - if (!didSendResponse) { - executePromise - // eslint-disable-next-line complexity - .then(async (data) => { - if (data === undefined) { - if (!didSendResponse) { - responseCallback(null, { - data: { - message: 'Workflow executed successfully but no data was returned', - }, - responseCode, - }); - didSendResponse = true; - } - return undefined; - } + executePromise + // eslint-disable-next-line complexity + .then(async (data) => { + if (data === undefined) { + responsePromise.resolve({ + response: { + body: { + message: 'Workflow executed successfully but no data was returned', + }, + statusCode: responseCodeParam, + headers: {}, + }, + }); + return; + } - if (usePinData) { - data.data.resultData.pinData = pinData; - } - - const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); - if (data.data.resultData.error || returnData?.error !== undefined) { - if (!didSendResponse) { - responseCallback(null, { - data: { - message: 'Error in workflow', - }, - responseCode: 500, - }); - } - didSendResponse = true; - return data; - } - - // in `responseNode` mode `responseCallback` is called by `responsePromise` - if (responseMode === 'responseNode' && responsePromise) { - await Promise.allSettled([responsePromise.promise]); - return undefined; - } - - if (returnData === undefined) { - if (!didSendResponse) { - responseCallback(null, { - data: { - message: - 'Workflow executed successfully but the last node did not return any data', - }, - responseCode, - }); - } - didSendResponse = true; - return data; - } - - const additionalKeys: IWorkflowDataProxyAdditionalKeys = { - $executionId: executionId, - }; - - if (!didSendResponse) { - let data: IDataObject | IDataObject[] | undefined; - - if (responseData === 'firstEntryJson') { - // Return the JSON data of the first entry - - if (returnData.data!.main[0]![0] === undefined) { - responseCallback(new ApplicationError('No item to return got found'), {}); - didSendResponse = true; - return undefined; - } - - data = returnData.data!.main[0]![0].json; - - const responsePropertyName = workflow.expression.getSimpleParameterValue( - workflowStartNode, - webhookData.webhookDescription.responsePropertyName, - executionMode, - additionalKeys, - undefined, - undefined, - ); - - if (responsePropertyName !== undefined) { - data = get(data, responsePropertyName as string) as IDataObject; - } - - const responseContentType = workflow.expression.getSimpleParameterValue( - workflowStartNode, - webhookData.webhookDescription.responseContentType, - executionMode, - additionalKeys, - undefined, - undefined, - ); - - if (responseContentType !== undefined) { - // Send the webhook response manually to be able to set the content-type - res.setHeader('Content-Type', responseContentType as string); - - // Returning an object, boolean, number, ... causes problems so make sure to stringify if needed - if ( - data !== null && - data !== undefined && - ['Buffer', 'String'].includes(data.constructor.name) - ) { - res.end(data); - } else { - res.end(JSON.stringify(data)); - } - - responseCallback(null, { - noWebhookResponse: true, - }); - didSendResponse = true; - } - } else if (responseData === 'firstEntryBinary') { - // Return the binary data of the first entry - data = returnData.data!.main[0]![0]; - - if (data === undefined) { - responseCallback(new ApplicationError('No item was found to return'), {}); - didSendResponse = true; - return undefined; - } - - if (data.binary === undefined) { - responseCallback(new ApplicationError('No binary data was found to return'), {}); - didSendResponse = true; - return undefined; - } - - const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue( - workflowStartNode, - webhookData.webhookDescription.responseBinaryPropertyName, - executionMode, - additionalKeys, - undefined, - 'data', - ); - - if (responseBinaryPropertyName === undefined && !didSendResponse) { - responseCallback( - new ApplicationError("No 'responseBinaryPropertyName' is set"), - {}, - ); - didSendResponse = true; - } - - const binaryData = (data.binary as IBinaryKeyData)[ - responseBinaryPropertyName as string - ]; - if (binaryData === undefined && !didSendResponse) { - responseCallback( - new ApplicationError( - `The binary property '${responseBinaryPropertyName}' which should be returned does not exist`, - ), - {}, - ); - didSendResponse = true; - } - - if (!didSendResponse) { - // Send the webhook response manually - res.setHeader('Content-Type', binaryData.mimeType); - if (binaryData.id) { - const stream = await Container.get(BinaryDataService).getAsStream(binaryData.id); - stream.pipe(res, { end: false }); - await finished(stream); - } else { - res.write(Buffer.from(binaryData.data, BINARY_ENCODING)); - } - - responseCallback(null, { - noWebhookResponse: true, - }); - process.nextTick(() => res.end()); - } - } else if (responseData === 'noData') { - // Return without data - data = undefined; - } else { - // Return the JSON data of all the entries - data = []; - for (const entry of returnData.data!.main[0]!) { - data.push(entry.json); - } - } - - if (!didSendResponse) { - responseCallback(null, { - data, - responseCode, - }); - } - } - didSendResponse = true; + if (usePinData) { + data.data.resultData.pinData = pinData; + } + const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); + if (data.data.resultData.error || returnData?.error !== undefined) { + responsePromise.resolve({ + response: { + body: { + message: 'Error in workflow', + }, + statusCode: 500, + headers: {}, + }, + }); return data; - }) - .catch((e) => { - if (!didSendResponse) { - responseCallback( - new ApplicationError('There was a problem executing the workflow', { - level: 'warning', - cause: e, - }), - {}, - ); + } + + // in `responseNode` mode `responseCallback` is called by `responsePromise` + if (responseMode === 'responseNode' && responsePromise) { + await Promise.allSettled([responsePromise.promise]); + return undefined; + } + + if (returnData === undefined) { + responsePromise.resolve({ + response: { + body: { + message: 'Workflow executed successfully but the last node did not return any data', + }, + statusCode: responseCodeParam, + headers: {}, + }, + }); + return data; + } + + const additionalKeys: IWorkflowDataProxyAdditionalKeys = { + $executionId: executionId, + }; + + let responseData: IDataObject | IDataObject[] | undefined; + + if (responseDataParam === 'firstEntryJson') { + // Return the JSON data of the first entry + + if (returnData.data!.main[0]![0] === undefined) { + responsePromise.reject(new ApplicationError('No item to return got found')); + return; } - const internalServerError = new InternalServerError(e.message, e); - if (e instanceof ExecutionCancelledError) internalServerError.level = 'warning'; - throw internalServerError; + responseData = returnData.data!.main[0]![0].json; + + const responsePropertyName = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responsePropertyName, + executionMode, + additionalKeys, + undefined, + undefined, + ); + + if (responsePropertyName !== undefined) { + responseData = get(responseData, responsePropertyName as string) as IDataObject; + } + + const responseContentType = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseContentType, + executionMode, + additionalKeys, + undefined, + undefined, + ); + + if (responseContentType !== undefined) { + // Send the webhook response manually to be able to set the content-type + res.setHeader('Content-Type', responseContentType as string); + + // Returning an object, boolean, number, ... causes problems so make sure to stringify if needed + if ( + responseData !== null && + responseData !== undefined && + ['Buffer', 'String'].includes(responseData.constructor.name) + ) { + res.end(responseData); + } else { + res.end(JSON.stringify(responseData)); + } + + responsePromise.resolve({ noWebhookResponse: true }); + return; + } + } else if (responseDataParam === 'firstEntryBinary') { + // Return the binary data of the first entry + responseData = returnData.data!.main[0]![0]; + + if (responseData === undefined) { + responsePromise.reject(new ApplicationError('No item was found to return')); + return; + } + + if (responseData.binary === undefined) { + responsePromise.reject(new ApplicationError('No binary data was found to return')); + return; + } + + const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue( + workflowStartNode, + webhookData.webhookDescription.responseBinaryPropertyName, + executionMode, + additionalKeys, + undefined, + 'data', + ); + + if (responseBinaryPropertyName === undefined) { + responsePromise.reject(new ApplicationError("No 'responseBinaryPropertyName' is set")); + return; + } + + const binaryData = (responseData.binary as IBinaryKeyData)[ + responseBinaryPropertyName as string + ]; + if (binaryData === undefined) { + responsePromise.reject( + new ApplicationError( + `The binary property '${responseBinaryPropertyName}' which should be returned does not exist`, + ), + ); + return; + } + + // Send the webhook response manually + res.setHeader('Content-Type', binaryData.mimeType); + if (binaryData.id) { + const stream = await Container.get(BinaryDataService).getAsStream(binaryData.id); + stream.pipe(res, { end: false }); + await finished(stream); + } else { + res.write(Buffer.from(binaryData.data, BINARY_ENCODING)); + } + + responsePromise.resolve({ noWebhookResponse: true }); + process.nextTick(() => res.end()); + } else if (responseDataParam === 'noData') { + // Return without data + responseData = undefined; + } else { + // Return the JSON data of all the entries + responseData = []; + for (const entry of returnData.data!.main[0]!) { + responseData.push(entry.json); + } + } + + responsePromise.resolve({ + response: { + body: responseData, + statusCode: responseCodeParam, + headers: {}, + }, }); - } + + return data; + }) + .catch((e) => { + responsePromise.reject( + new ApplicationError('There was a problem executing the workflow', { + level: 'warning', + cause: e, + }), + ); + + const internalServerError = new InternalServerError(e.message, e); + if (e instanceof ExecutionCancelledError) internalServerError.level = 'warning'; + throw internalServerError; + }); return executionId; } catch (e) { const error = @@ -826,8 +776,7 @@ export async function executeWebhook( level: 'warning', cause: e, }); - if (didSendResponse) throw error; - responseCallback(error, {}); + responsePromise.reject(error); return; } } diff --git a/packages/cli/src/webhooks/webhook-request-handler.ts b/packages/cli/src/webhooks/webhook-request-handler.ts index 8eb62358b1..8fddb7f0ac 100644 --- a/packages/cli/src/webhooks/webhook-request-handler.ts +++ b/packages/cli/src/webhooks/webhook-request-handler.ts @@ -42,18 +42,7 @@ class WebhookRequestHandler { } try { - const response = await this.webhookManager.executeWebhook(req, res); - - // Don't respond, if already responded - if (response.noWebhookResponse !== true) { - ResponseHelper.sendSuccessResponse( - res, - response.data, - true, - response.responseCode, - response.headers, - ); - } + await this.webhookManager.executeWebhook(req, res); } catch (e) { const error = ensureError(e); Container.get(Logger).debug( diff --git a/packages/cli/src/webhooks/webhook.types.ts b/packages/cli/src/webhooks/webhook.types.ts index 4d34b00d04..8a61014027 100644 --- a/packages/cli/src/webhooks/webhook.types.ts +++ b/packages/cli/src/webhooks/webhook.types.ts @@ -1,5 +1,5 @@ import type { Request, Response } from 'express'; -import type { IDataObject, IHttpRequestMethods } from 'n8n-workflow'; +import type { IHttpRequestMethods, IN8nHttpFullResponse } from 'n8n-workflow'; export type WebhookOptionsRequest = Request & { method: 'OPTIONS' }; @@ -26,12 +26,12 @@ export interface IWebhookManager { httpMethod: IHttpRequestMethods, ) => Promise; - executeWebhook(req: WebhookRequest, res: Response): Promise; + executeWebhook(req: WebhookRequest, res: Response): Promise; } -export interface IWebhookResponseCallbackData { - data?: IDataObject | IDataObject[]; - headers?: object; - noWebhookResponse?: boolean; - responseCode?: number; -} +export type IWebhookResponsePromiseData = + | { noWebhookResponse: true } + | { + noWebhookResponse?: false; + response: IN8nHttpFullResponse; + }; diff --git a/packages/cli/test/integration/webhooks.test.ts b/packages/cli/test/integration/webhooks.test.ts index 0895f3caa8..ed849f1d2e 100644 --- a/packages/cli/test/integration/webhooks.test.ts +++ b/packages/cli/test/integration/webhooks.test.ts @@ -1,6 +1,5 @@ import { GlobalConfig } from '@n8n/config'; import { Container } from '@n8n/di'; -import { mock } from 'jest-mock-extended'; import { agent as testAgent } from 'supertest'; import type SuperAgentTest from 'supertest/lib/agent'; @@ -10,7 +9,6 @@ import { TestWebhooks } from '@/webhooks/test-webhooks'; import { WaitingForms } from '@/webhooks/waiting-forms'; import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; import { WebhookServer } from '@/webhooks/webhook-server'; -import type { IWebhookResponseCallbackData } from '@/webhooks/webhook.types'; import { mockInstance } from '@test/mocking'; let agent: SuperAgentTest; @@ -60,9 +58,6 @@ describe('WebhookServer', () => { it('should handle regular requests', async () => { const pathPrefix = Container.get(GlobalConfig).endpoints[key]; manager.getWebhookMethods.mockResolvedValueOnce(['GET']); - manager.executeWebhook.mockResolvedValueOnce( - mockResponse({ test: true }, { key: 'value ' }), - ); const response = await agent .get(`/${pathPrefix}/abcd`) @@ -75,13 +70,5 @@ describe('WebhookServer', () => { }); }); } - - const mockResponse = (data = {}, headers = {}, status = 200) => { - const response = mock(); - response.responseCode = status; - response.data = data; - response.headers = headers; - return response; - }; }); }); diff --git a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts index b8038d9163..a96a0732bd 100644 --- a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts +++ b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts @@ -23,6 +23,12 @@ import type { Readable } from 'stream'; import { formatPrivateKey, generatePairedItemData } from '../../utils/utilities'; +type Options = { + responseHeaders: { entries: Array<{ name: string; value: string }> }; + responseCode: number; + responseKey: string; +}; + export class RespondToWebhook implements INodeType { description: INodeTypeDescription = { displayName: 'Respond to Webhook', @@ -323,19 +329,16 @@ export class RespondToWebhook implements INodeType { } const respondWith = this.getNodeParameter('respondWith', 0) as string; - const options = this.getNodeParameter('options', 0, {}); + const options = this.getNodeParameter('options', 0, {}) as Options; - const headers = {} as IDataObject; - if (options.responseHeaders) { - for (const header of (options.responseHeaders as IDataObject).entries as IDataObject[]) { - if (typeof header.name !== 'string') { - header.name = header.name?.toString(); - } - headers[header.name?.toLowerCase() as string] = header.value?.toString(); + const headers: IN8nHttpFullResponse['headers'] = {}; + if (options.responseHeaders?.entries?.length) { + for (const header of options.responseHeaders.entries) { + headers[String(header.name).toLowerCase()] = String(header.value); } } - let statusCode = (options.responseCode as number) || 200; + let statusCode = options.responseCode ?? 200; let responseBody: IN8nHttpResponse | Readable; if (respondWith === 'json') { const responseBodyParameter = this.getNodeParameter('responseBody', 0) as string; @@ -381,11 +384,11 @@ export class RespondToWebhook implements INodeType { } else if (respondWith === 'allIncomingItems') { const respondItems = items.map((item) => item.json); responseBody = options.responseKey - ? set({}, options.responseKey as string, respondItems) + ? set({}, options.responseKey, respondItems) : respondItems; } else if (respondWith === 'firstIncomingItem') { responseBody = options.responseKey - ? set({}, options.responseKey as string, items[0].json) + ? set({}, options.responseKey, items[0].json) : items[0].json; } else if (respondWith === 'text') { responseBody = this.getNodeParameter('responseBody', 0) as string; @@ -418,7 +421,7 @@ export class RespondToWebhook implements INodeType { responseBody = { binaryData }; } else { responseBody = Buffer.from(binaryData.data, BINARY_ENCODING); - headers['content-length'] = (responseBody as Buffer).length; + headers['content-length'] = String((responseBody as Buffer).length); } if (!headers['content-type']) { @@ -426,7 +429,7 @@ export class RespondToWebhook implements INodeType { } } else if (respondWith === 'redirect') { headers.location = this.getNodeParameter('redirectURL', 0) as string; - statusCode = (options.responseCode as number) ?? 307; + statusCode = options.responseCode ?? 307; } else if (respondWith !== 'noData') { throw new NodeOperationError( this.getNode(), diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 9ef38c4532..b37395bbfa 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -518,10 +518,12 @@ export interface PaginationOptions { export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[] | null; +type ResponseHeaders = Record; + export interface IN8nHttpFullResponse { body: IN8nHttpResponse | Readable; __bodyResolved?: boolean; - headers: IDataObject; + headers: ResponseHeaders; statusCode: number; statusMessage?: string; }