This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2025-03-05 11:09:43 +01:00 committed by GitHub
commit b60955379c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 463 additions and 553 deletions

View file

@ -6,7 +6,7 @@ import type { ExecutionRepository } from '@/databases/repositories/execution.rep
import { WaitingForms } from '@/webhooks/waiting-forms'; import { WaitingForms } from '@/webhooks/waiting-forms';
import type { IExecutionResponse } from '../../interfaces'; import type { IExecutionResponse } from '../../interfaces';
import type { WaitingWebhookRequest } from '../webhook.types'; import type { IWebhookResponsePromiseData, WaitingWebhookRequest } from '../webhook.types';
describe('WaitingForms', () => { describe('WaitingForms', () => {
const executionRepository = mock<ExecutionRepository>(); const executionRepository = mock<ExecutionRepository>();
@ -205,7 +205,7 @@ describe('WaitingForms', () => {
// @ts-expect-error Protected method // @ts-expect-error Protected method
.spyOn(waitingForms, 'getWebhookExecutionData') .spyOn(waitingForms, 'getWebhookExecutionData')
// @ts-expect-error Protected method // @ts-expect-error Protected method
.mockResolvedValue(mock<IWebhookResponseCallbackData>()); .mockResolvedValue(mock<IWebhookResponsePromiseData>());
const execution = mock<IExecutionResponse>({ const execution = mock<IExecutionResponse>({
finished: false, finished: false,

View file

@ -6,7 +6,7 @@ import { ConflictError } from '@/errors/response-errors/conflict.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import type { IExecutionResponse } from '@/interfaces'; import type { IExecutionResponse } from '@/interfaces';
import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; import { WaitingWebhooks } from '@/webhooks/waiting-webhooks';
import type { IWebhookResponseCallbackData, WaitingWebhookRequest } from '@/webhooks/webhook.types'; import type { IWebhookResponsePromiseData, WaitingWebhookRequest } from '@/webhooks/webhook.types';
describe('WaitingWebhooks', () => { describe('WaitingWebhooks', () => {
const executionRepository = mock<ExecutionRepository>(); const executionRepository = mock<ExecutionRepository>();
@ -85,7 +85,7 @@ describe('WaitingWebhooks', () => {
// @ts-expect-error Protected method // @ts-expect-error Protected method
.spyOn(waitingWebhooks, 'getWebhookExecutionData') .spyOn(waitingWebhooks, 'getWebhookExecutionData')
// @ts-expect-error Protected method // @ts-expect-error Protected method
.mockResolvedValue(mock<IWebhookResponseCallbackData>()); .mockResolvedValue(mock<IWebhookResponsePromiseData>());
const execution = mock<IExecutionResponse>({ const execution = mock<IExecutionResponse>({
finished: false, finished: false,

View file

@ -1,9 +1,8 @@
import { mock, type MockProxy } from 'jest-mock-extended'; 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 { FORM_NODE_TYPE, WAIT_NODE_TYPE } from 'n8n-workflow';
import { autoDetectResponseMode, handleFormRedirectionCase } from '../webhook-helpers'; import { autoDetectResponseMode, handleFormRedirectionCase } from '../webhook-helpers';
import type { IWebhookResponseCallbackData } from '../webhook.types';
describe('autoDetectResponseMode', () => { describe('autoDetectResponseMode', () => {
let workflow: MockProxy<Workflow>; let workflow: MockProxy<Workflow>;
@ -58,40 +57,46 @@ describe('autoDetectResponseMode', () => {
describe('handleFormRedirectionCase', () => { describe('handleFormRedirectionCase', () => {
test('should return data unchanged if start node is WAIT_NODE_TYPE with resume not equal to form', () => { test('should return data unchanged if start node is WAIT_NODE_TYPE with resume not equal to form', () => {
const data: IWebhookResponseCallbackData = { const response: IN8nHttpFullResponse = {
responseCode: 302, statusCode: 302,
headers: { location: 'http://example.com' }, headers: { location: 'http://example.com' },
body: {},
}; };
const workflowStartNode = mock<INode>({ const workflowStartNode = mock<INode>({
type: WAIT_NODE_TYPE, type: WAIT_NODE_TYPE,
parameters: { resume: 'webhook' }, parameters: { resume: 'webhook' },
}); });
const result = handleFormRedirectionCase(data, workflowStartNode); const result = handleFormRedirectionCase(response, workflowStartNode);
expect(result).toEqual(data); expect(result).toEqual(response);
}); });
test('should modify data if start node type matches and responseCode is a redirect', () => { test('should modify data if start node type matches and responseCode is a redirect', () => {
const data: IWebhookResponseCallbackData = { const response: IN8nHttpFullResponse = {
responseCode: 302, statusCode: 302,
headers: { location: 'http://example.com' }, headers: { location: 'http://example.com' },
body: {},
}; };
const workflowStartNode = mock<INode>({ const workflowStartNode = mock<INode>({
type: FORM_NODE_TYPE, type: FORM_NODE_TYPE,
parameters: {}, parameters: {},
}); });
const result = handleFormRedirectionCase(data, workflowStartNode); const result = handleFormRedirectionCase(response, workflowStartNode);
expect(result.responseCode).toBe(200); expect(result.statusCode).toBe(200);
expect(result.data).toEqual({ redirectURL: 'http://example.com' }); expect(result.body).toEqual({ redirectURL: 'http://example.com' });
expect((result?.headers as IDataObject)?.location).toBeUndefined(); expect(result.headers.location).toBeUndefined();
}); });
test('should not modify data if location header is missing', () => { 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<INode>({ const workflowStartNode = mock<INode>({
type: FORM_NODE_TYPE, type: FORM_NODE_TYPE,
parameters: {}, parameters: {},
}); });
const result = handleFormRedirectionCase(data, workflowStartNode); const result = handleFormRedirectionCase(response, workflowStartNode);
expect(result).toEqual(data); expect(result).toEqual(response);
}); });
}); });

View file

@ -7,7 +7,7 @@ import { ResponseError } from '@/errors/response-errors/abstract/response.error'
import { createWebhookHandlerFor } from '@/webhooks/webhook-request-handler'; import { createWebhookHandlerFor } from '@/webhooks/webhook-request-handler';
import type { import type {
IWebhookManager, IWebhookManager,
IWebhookResponseCallbackData, IWebhookResponsePromiseData,
WebhookOptionsRequest, WebhookOptionsRequest,
WebhookRequest, WebhookRequest,
} from '@/webhooks/webhook.types'; } from '@/webhooks/webhook.types';
@ -150,14 +150,16 @@ describe('WebhookRequestHandler', () => {
const res = mock<Response>(); const res = mock<Response>();
const executeWebhookResponse: IWebhookResponseCallbackData = { const executeWebhookResponse: IWebhookResponsePromiseData = {
responseCode: 200, response: {
data: {}, statusCode: 200,
body: {},
headers: { headers: {
'x-custom-header': 'test', 'x-custom-header': 'test',
}, },
},
}; };
webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse); // webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse);
await handler(req, res); await handler(req, res);
@ -166,7 +168,7 @@ describe('WebhookRequestHandler', () => {
expect(res.header).toHaveBeenCalledWith({ expect(res.header).toHaveBeenCalledWith({
'x-custom-header': 'test', '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 () => { it('should send an error response if webhook execution throws', async () => {
@ -204,16 +206,20 @@ describe('WebhookRequestHandler', () => {
const res = mock<Response>(); const res = mock<Response>();
const executeWebhookResponse: IWebhookResponseCallbackData = { const executeWebhookResponse: IWebhookResponsePromiseData = {
responseCode: 200, response: {
statusCode: 200,
headers: {},
body: {},
},
}; };
webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse); // webhookManager.executeWebhook.mockResolvedValueOnce(executeWebhookResponse);
await handler(req, res); await handler(req, res);
expect(webhookManager.executeWebhook).toHaveBeenCalledWith(req, res); expect(webhookManager.executeWebhook).toHaveBeenCalledWith(req, res);
expect(res.status).toHaveBeenCalledWith(200); expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(executeWebhookResponse.data); expect(res.json).toHaveBeenCalledWith(executeWebhookResponse.response.body);
}, },
); );
}); });

View file

@ -1,7 +1,7 @@
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type { Response } from 'express'; import type { Response } from 'express';
import { Logger } from 'n8n-core'; 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 type { INode, IWebhookData, IHttpRequestMethods } from 'n8n-workflow';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; 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 { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
import type { import type {
IWebhookResponseCallbackData, IWebhookResponsePromiseData,
IWebhookManager, IWebhookManager,
WebhookAccessControlOptions, WebhookAccessControlOptions,
WebhookRequest, 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. * Checks if a webhook for the given method and path exists and executes the workflow.
*/ */
async executeWebhook( async executeWebhook(request: WebhookRequest, response: Response): Promise<void> {
request: WebhookRequest,
response: Response,
): Promise<IWebhookResponseCallbackData> {
const httpMethod = request.method; const httpMethod = request.method;
const path = request.params.path; const path = request.params.path;
@ -126,9 +123,9 @@ export class LiveWebhooks implements IWebhookManager {
throw new NotFoundError('Could not find node to process webhook.'); throw new NotFoundError('Could not find node to process webhook.');
} }
return await new Promise((resolve, reject) => {
const executionMode = 'webhook'; const executionMode = 'webhook';
void WebhookHelpers.executeWebhook( const responsePromise = createDeferredPromise<IWebhookResponsePromiseData>();
await WebhookHelpers.executeWebhook(
workflow, workflow,
webhookData, webhookData,
workflowData, workflowData,
@ -139,16 +136,11 @@ export class LiveWebhooks implements IWebhookManager {
undefined, undefined,
request, request,
response, response,
async (error: Error | null, data: object) => { responsePromise,
if (error !== null) { );
return reject(error);
}
// Save static data if it changed // Save static data if it changed
await this.workflowStaticDataService.saveStaticData(workflow); await this.workflowStaticDataService.saveStaticData(workflow);
resolve(data);
},
);
});
} }
private async findWebhook(path: string, httpMethod: IHttpRequestMethods) { private async findWebhook(path: string, httpMethod: IHttpRequestMethods) {

View file

@ -1,7 +1,7 @@
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type express from 'express'; import type express from 'express';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { WebhookPathTakenError, Workflow } from 'n8n-workflow'; import { createDeferredPromise, WebhookPathTakenError, Workflow } from 'n8n-workflow';
import type { import type {
IWebhookData, IWebhookData,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
@ -26,7 +26,7 @@ import type { WorkflowRequest } from '@/workflows/workflow.request';
import { WebhookService } from './webhook.service'; import { WebhookService } from './webhook.service';
import type { import type {
IWebhookResponseCallbackData, IWebhookResponsePromiseData,
IWebhookManager, IWebhookManager,
WebhookAccessControlOptions, WebhookAccessControlOptions,
WebhookRequest, WebhookRequest,
@ -53,10 +53,7 @@ export class TestWebhooks implements IWebhookManager {
* Return a promise that resolves when the test webhook is called. * Return a promise that resolves when the test webhook is called.
* Also inform the FE of the result and remove the test webhook. * Also inform the FE of the result and remove the test webhook.
*/ */
async executeWebhook( async executeWebhook(request: WebhookRequest, response: express.Response): Promise<void> {
request: WebhookRequest,
response: express.Response,
): Promise<IWebhookResponseCallbackData> {
const httpMethod = request.method; const httpMethod = request.method;
let path = removeTrailingSlash(request.params.path); let path = removeTrailingSlash(request.params.path);
@ -113,9 +110,9 @@ export class TestWebhooks implements IWebhookManager {
throw new NotFoundError('Could not find node to process webhook.'); throw new NotFoundError('Could not find node to process webhook.');
} }
return await new Promise(async (resolve, reject) => {
try {
const executionMode = 'manual'; const executionMode = 'manual';
const responsePromise = createDeferredPromise<IWebhookResponsePromiseData>();
try {
const executionId = await WebhookHelpers.executeWebhook( const executionId = await WebhookHelpers.executeWebhook(
workflow, workflow,
webhook, webhook,
@ -127,10 +124,7 @@ export class TestWebhooks implements IWebhookManager {
undefined, // executionId undefined, // executionId
request, request,
response, response,
(error: Error | null, data: IWebhookResponseCallbackData) => { responsePromise,
if (error !== null) reject(error);
else resolve(data);
},
destinationNode, destinationNode,
); );
@ -165,7 +159,6 @@ export class TestWebhooks implements IWebhookManager {
this.clearTimeout(key); this.clearTimeout(key);
await this.deactivateWebhooks(workflow); await this.deactivateWebhooks(workflow);
});
} }
clearTimeout(key: string) { clearTimeout(key: string) {

View file

@ -8,7 +8,7 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
import type { IExecutionResponse } from '@/interfaces'; import type { IExecutionResponse } from '@/interfaces';
import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; import { WaitingWebhooks } from '@/webhooks/waiting-webhooks';
import type { IWebhookResponseCallbackData, WaitingWebhookRequest } from './webhook.types'; import type { WaitingWebhookRequest } from './webhook.types';
@Service() @Service()
export class WaitingForms extends WaitingWebhooks { export class WaitingForms extends WaitingWebhooks {
@ -61,10 +61,7 @@ export class WaitingForms extends WaitingWebhooks {
} }
} }
async executeWebhook( async executeWebhook(req: WaitingWebhookRequest, res: express.Response): Promise<void> {
req: WaitingWebhookRequest,
res: express.Response,
): Promise<IWebhookResponseCallbackData> {
const { path: executionId, suffix } = req.params; const { path: executionId, suffix } = req.params;
this.logReceivedWebhook(req.method, executionId); this.logReceivedWebhook(req.method, executionId);
@ -107,10 +104,7 @@ export class WaitingForms extends WaitingWebhooks {
message: 'Your response has been recorded', message: 'Your response has been recorded',
formTitle: 'Form Submitted', formTitle: 'Form Submitted',
}); });
return;
return {
noWebhookResponse: true,
};
} else { } else {
lastNodeExecuted = completionPage; lastNodeExecuted = completionPage;
} }
@ -122,7 +116,7 @@ export class WaitingForms extends WaitingWebhooks {
*/ */
if (execution.mode === 'manual') execution.data.isTestWebhook = true; if (execution.mode === 'manual') execution.data.isTestWebhook = true;
return await this.getWebhookExecutionData({ await this.getWebhookExecutionData({
execution, execution,
req, req,
res, res,

View file

@ -1,10 +1,10 @@
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type express from 'express'; import type express from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
import type { INodes, IWorkflowBase } from 'n8n-workflow';
import { import {
createDeferredPromise,
FORM_NODE_TYPE, FORM_NODE_TYPE,
type INodes,
type IWorkflowBase,
SEND_AND_WAIT_OPERATION, SEND_AND_WAIT_OPERATION,
WAIT_NODE_TYPE, WAIT_NODE_TYPE,
Workflow, Workflow,
@ -20,7 +20,7 @@ import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-da
import { WebhookService } from './webhook.service'; import { WebhookService } from './webhook.service';
import type { import type {
IWebhookResponseCallbackData, IWebhookResponsePromiseData,
IWebhookManager, IWebhookManager,
WaitingWebhookRequest, WaitingWebhookRequest,
} from './webhook.types'; } from './webhook.types';
@ -81,10 +81,7 @@ export class WaitingWebhooks implements IWebhookManager {
}); });
} }
async executeWebhook( async executeWebhook(req: WaitingWebhookRequest, res: express.Response): Promise<void> {
req: WaitingWebhookRequest,
res: express.Response,
): Promise<IWebhookResponseCallbackData> {
const { path: executionId, suffix } = req.params; const { path: executionId, suffix } = req.params;
this.logReceivedWebhook(req.method, executionId); this.logReceivedWebhook(req.method, executionId);
@ -113,7 +110,7 @@ export class WaitingWebhooks implements IWebhookManager {
const { nodes } = this.createWorkflow(workflowData); const { nodes } = this.createWorkflow(workflowData);
if (this.isSendAndWaitRequest(nodes, suffix)) { if (this.isSendAndWaitRequest(nodes, suffix)) {
res.render('send-and-wait-no-action-required', { isTestWebhook: false }); res.render('send-and-wait-no-action-required', { isTestWebhook: false });
return { noWebhookResponse: true }; return;
} else { } else {
throw new ConflictError(`The execution "${executionId} has finished already.`); 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; if (execution.mode === 'manual') execution.data.isTestWebhook = true;
return await this.getWebhookExecutionData({ await this.getWebhookExecutionData({
execution, execution,
req, req,
res, res,
@ -151,7 +148,7 @@ export class WaitingWebhooks implements IWebhookManager {
lastNodeExecuted: string; lastNodeExecuted: string;
executionId: string; executionId: string;
suffix?: string; suffix?: string;
}): Promise<IWebhookResponseCallbackData> { }): Promise<void> {
// Set the node as disabled so that the data does not get executed again as it would result // 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 // in starting the wait all over again
this.disableNode(execution, req.method); this.disableNode(execution, req.method);
@ -188,7 +185,7 @@ export class WaitingWebhooks implements IWebhookManager {
if (this.isSendAndWaitRequest(workflow.nodes, suffix)) { if (this.isSendAndWaitRequest(workflow.nodes, suffix)) {
res.render('send-and-wait-no-action-required', { isTestWebhook: false }); res.render('send-and-wait-no-action-required', { isTestWebhook: false });
return { noWebhookResponse: true }; return;
} }
if (!execution.data.resultData.error && execution.status === 'waiting') { if (!execution.data.resultData.error && execution.status === 'waiting') {
@ -203,7 +200,7 @@ export class WaitingWebhooks implements IWebhookManager {
); );
if (hasChildForms) { if (hasChildForms) {
return { noWebhookResponse: true }; return;
} }
} }
@ -212,9 +209,9 @@ export class WaitingWebhooks implements IWebhookManager {
const runExecutionData = execution.data; const runExecutionData = execution.data;
return await new Promise((resolve, reject) => {
const executionMode = 'webhook'; const executionMode = 'webhook';
void WebhookHelpers.executeWebhook( const responsePromise = createDeferredPromise<IWebhookResponsePromiseData>();
await WebhookHelpers.executeWebhook(
workflow, workflow,
webhookData, webhookData,
workflowData, workflowData,
@ -225,14 +222,7 @@ export class WaitingWebhooks implements IWebhookManager {
execution.id, execution.id,
req, req,
res, res,
responsePromise,
(error: Error | null, data: object) => {
if (error !== null) {
return reject(error);
}
resolve(data);
},
); );
});
} }
} }

View file

@ -34,7 +34,6 @@ import type {
import { import {
ApplicationError, ApplicationError,
BINARY_ENCODING, BINARY_ENCODING,
createDeferredPromise,
ExecutionCancelledError, ExecutionCancelledError,
FORM_NODE_TYPE, FORM_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE,
@ -60,7 +59,7 @@ import * as WorkflowHelpers from '@/workflow-helpers';
import { WorkflowRunner } from '@/workflow-runner'; import { WorkflowRunner } from '@/workflow-runner';
import { WebhookService } from './webhook.service'; 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 * Returns all the webhooks which should be created for the given workflow
@ -107,7 +106,7 @@ export function autoDetectResponseMode(
workflowStartNode: INode, workflowStartNode: INode,
workflow: Workflow, workflow: Workflow,
method: string, method: string,
) { ): WebhookResponseMode | undefined {
if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') { if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') {
return undefined; 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 * for formTrigger and form nodes redirection has to be handled by sending redirectURL in response body
*/ */
export const handleFormRedirectionCase = ( export const handleFormRedirectionCase = (
data: IWebhookResponseCallbackData, response: IN8nHttpFullResponse,
workflowStartNode: INode, workflowStartNode: INode,
) => { ): IN8nHttpFullResponse => {
if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') { if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') {
return data; return response;
} }
const { headers, statusCode } = response;
if ( if (
[FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(workflowStartNode.type) && [FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(workflowStartNode.type) &&
(data?.headers as IDataObject)?.location && headers.location &&
String(data?.responseCode).startsWith('3') String(statusCode).startsWith('3')
) { ) {
data.responseCode = 200; response.statusCode = 200;
data.data = { response.body = { redirectURL: headers.location };
redirectURL: (data?.headers as IDataObject)?.location, delete headers.location;
};
(data.headers as IDataObject).location = undefined;
} }
return data; return response;
}; };
const { formDataFileSizeMax } = Container.get(GlobalConfig).endpoints; const { formDataFileSizeMax } = Container.get(GlobalConfig).endpoints;
@ -177,7 +175,7 @@ export async function executeWebhook(
executionId: string | undefined, executionId: string | undefined,
req: WebhookRequest, req: WebhookRequest,
res: express.Response, res: express.Response,
responseCallback: (error: Error | null, data: IWebhookResponseCallbackData) => void, responsePromise: IDeferredPromise<IWebhookResponsePromiseData>,
destinationNode?: string, destinationNode?: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
// Get the nodeType to know which responseMode is set // Get the nodeType to know which responseMode is set
@ -185,11 +183,6 @@ export async function executeWebhook(
workflowStartNode.type, workflowStartNode.type,
workflowStartNode.typeVersion, 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 = { const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId, $executionId: executionId,
@ -209,24 +202,28 @@ export async function executeWebhook(
additionalData.executionId = executionId; additionalData.executionId = executionId;
} }
// Get the responseMode
let responseMode;
//check if response mode should be set automatically, e.g. multipage form //check if response mode should be set automatically, e.g. multipage form
responseMode = autoDetectResponseMode(workflowStartNode, workflow, req.method); const responseMode =
autoDetectResponseMode(workflowStartNode, workflow, req.method) ??
if (!responseMode) { (workflow.expression.getSimpleParameterValue(
responseMode = workflow.expression.getSimpleParameterValue(
workflowStartNode, workflowStartNode,
webhookData.webhookDescription.responseMode, webhookData.webhookDescription.responseMode,
executionMode, executionMode,
additionalKeys, additionalKeys,
undefined, undefined,
'onReceived', '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, workflowStartNode,
webhookData.webhookDescription.responseCode as string, webhookData.webhookDescription.responseCode as string,
executionMode, executionMode,
@ -235,7 +232,7 @@ export async function executeWebhook(
200, 200,
) as number; ) as number;
const responseData = workflow.expression.getComplexParameterValue( const responseDataParam = workflow.expression.getComplexParameterValue(
workflowStartNode, workflowStartNode,
webhookData.webhookDescription.responseData, webhookData.webhookDescription.responseData,
executionMode, executionMode,
@ -244,35 +241,24 @@ export async function executeWebhook(
'firstEntryJson', '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);
}
// 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; const nodeVersion = workflowStartNode.typeVersion;
if (nodeVersion === 1) {
// binaryData option is removed in versions higher than 1 // binaryData option is removed in versions higher than 1
binaryData = workflow.expression.getSimpleParameterValue( const binaryDataParam =
nodeVersion === 1
? (workflow.expression.getSimpleParameterValue(
workflowStartNode, workflowStartNode,
'={{$parameter["options"]["binaryData"]}}', '={{$parameter["options"]["binaryData"]}}',
executionMode, executionMode,
additionalKeys, additionalKeys,
undefined, undefined,
false, 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 didSendResponse = false;
let runExecutionDataMerge = {}; let runExecutionDataMerge = {};
try { try {
// Run the webhook function to see what should be returned and if // 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 // if `Webhook` or `Wait` node, and binaryData is enabled, skip pre-parse the request-body
// always falsy for versions higher than 1 // always falsy for versions higher than 1
if (!binaryData) { if (!binaryDataParam) {
const { contentType } = req; const { contentType } = req;
if (contentType === 'multipart/form-data') { if (contentType === 'multipart/form-data') {
req.body = await parseFormData(req); 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 // Add error to execution data that it can be logged and send to Editor-UI
runExecutionDataMerge = { runExecutionDataMerge = {
resultData: { resultData: {
@ -358,16 +341,20 @@ export async function executeWebhook(
// which then so gets the chance to throw the error. // which then so gets the chance to throw the error.
workflowData: [[{ json: {} }]], workflowData: [[{ json: {} }]],
}; };
responsePromise.reject(new ApplicationError(errorMessage));
return;
} }
const additionalKeys: IWorkflowDataProxyAdditionalKeys = { const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: executionId, $executionId: executionId,
}; };
const responseHeadersParam = webhookData.webhookDescription.responseHeaders;
if (webhookData.webhookDescription.responseHeaders !== undefined) { if (webhookData.webhookDescription.responseHeaders !== undefined) {
const responseHeaders = workflow.expression.getComplexParameterValue( const responseHeaders = workflow.expression.getComplexParameterValue(
workflowStartNode, workflowStartNode,
webhookData.webhookDescription.responseHeaders, responseHeadersParam,
executionMode, executionMode,
additionalKeys, additionalKeys,
undefined, undefined,
@ -381,90 +368,83 @@ export async function executeWebhook(
| undefined; | undefined;
}; };
if (responseHeaders !== undefined && responseHeaders.entries !== undefined) { if (responseHeaders?.entries?.length) {
for (const item of responseHeaders.entries) { for (const item of responseHeaders.entries) {
res.setHeader(item.name, item.value); res.setHeader(item.name, item.value);
} }
} }
} }
if (webhookResultData.noWebhookResponse === true && !didSendResponse) { if (webhookResultData.noWebhookResponse === true) {
// The response got already send // The response got already send
responseCallback(null, { responsePromise.resolve({ noWebhookResponse: true });
noWebhookResponse: true, return;
});
didSendResponse = true;
} }
if (webhookResultData.workflowData === undefined) { if (webhookResultData.workflowData === undefined) {
// Workflow should not run // Workflow should not run
if (webhookResultData.webhookResponse !== undefined) { if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given // Data to respond with is given
if (!didSendResponse) { responsePromise.resolve({
responseCallback(null, { response: {
data: webhookResultData.webhookResponse, statusCode: responseCodeParam,
responseCode, body: webhookResultData.webhookResponse,
headers: {},
},
}); });
didSendResponse = true; return;
}
} else { } else {
// Send default response // Send default response
responsePromise.resolve({
if (!didSendResponse) { response: {
responseCallback(null, { statusCode: responseCodeParam,
data: { body: { message: 'Webhook call received' },
message: 'Webhook call received', headers: {},
}, },
responseCode,
}); });
didSendResponse = true; return;
}
} }
return; return;
} }
// Now that we know that the workflow should run we can return the default response // 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 // directly if responseMode it set to "onReceived" and a response should be sent
if (responseMode === 'onReceived' && !didSendResponse) { if (responseMode === 'onReceived') {
// Return response directly and do not wait for the workflow to finish void responsePromise.promise.then(async (resolveData) => {
if (responseData === 'noData') { if (resolveData.noWebhookResponse) {
// Return without data // TODO: send 204
responseCallback(null, { res.end();
responseCode, return;
});
} 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,
});
} }
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 // Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = []; const nodeExecutionStack: IExecuteData[] = [
nodeExecutionStack.push({ {
node: workflowStartNode, node: workflowStartNode,
data: { data: {
main: webhookResultData.workflowData, main: webhookResultData.workflowData,
}, },
source: null, source: null,
}); },
];
runExecutionData = runExecutionData =
runExecutionData || runExecutionData ||
@ -517,43 +497,34 @@ export async function executeWebhook(
runData.pushRef = runExecutionData.pushRef; runData.pushRef = runExecutionData.pushRef;
} }
let responsePromise: IDeferredPromise<IN8nHttpFullResponse> | undefined;
if (responseMode === 'responseNode') { if (responseMode === 'responseNode') {
responsePromise = createDeferredPromise<IN8nHttpFullResponse>();
responsePromise.promise responsePromise.promise
.then(async (response: IN8nHttpFullResponse) => { .then(async (resolveData) => {
if (didSendResponse) { if (resolveData.noWebhookResponse) {
return; return;
} }
const { response } = resolveData;
const binaryData = (response.body as IDataObject)?.binaryData as IBinaryData; const binaryData = (response.body as IDataObject)?.binaryData as IBinaryData;
if (binaryData?.id) { if (binaryData?.id) {
res.header(response.headers); res.header(response.headers);
const stream = await Container.get(BinaryDataService).getAsStream(binaryData.id); const stream = await Container.get(BinaryDataService).getAsStream(binaryData.id);
stream.pipe(res, { end: false }); stream.pipe(res, { end: false });
await finished(stream); await finished(stream);
responseCallback(null, { noWebhookResponse: true }); responsePromise.resolve({ noWebhookResponse: true });
} else if (Buffer.isBuffer(response.body)) { } else if (Buffer.isBuffer(response.body)) {
res.header(response.headers); res.header(response.headers);
res.end(response.body); res.write(response.body);
responseCallback(null, { noWebhookResponse: true }); responsePromise.resolve({ noWebhookResponse: true });
} else { } else {
// TODO: This probably needs some more changes depending on the options on the // TODO: This probably needs some more changes depending on the options on the
// Webhook Response node // Webhook Response node
let data: IWebhookResponsePromiseData = { response };
let data: IWebhookResponseCallbackData = { data.response = handleFormRedirectionCase(data.response, workflowStartNode);
data: response.body as IDataObject, responsePromise.resolve(data);
headers: response.headers,
responseCode: response.statusCode,
};
data = handleFormRedirectionCase(data, workflowStartNode);
responseCallback(null, data);
} }
process.nextTick(() => res.end()); process.nextTick(() => res.end());
didSendResponse = true;
}) })
.catch(async (error) => { .catch(async (error) => {
Container.get(ErrorReporter).error(error); Container.get(ErrorReporter).error(error);
@ -561,7 +532,7 @@ export async function executeWebhook(
`Error with Webhook-Response for execution "${executionId}": "${error.message}"`, `Error with Webhook-Response for execution "${executionId}": "${error.message}"`,
{ executionId, workflowId: workflow.id }, { executionId, workflowId: workflow.id },
); );
responseCallback(error, {}); responsePromise.reject(error);
}); });
} }
@ -578,7 +549,7 @@ export async function executeWebhook(
executionId = await Container.get(WorkflowRunner).run( executionId = await Container.get(WorkflowRunner).run(
runData, runData,
true, true,
!didSendResponse, true,
executionId, executionId,
responsePromise, responsePromise,
); );
@ -602,21 +573,20 @@ export async function executeWebhook(
}); });
} }
if (!didSendResponse) {
executePromise executePromise
// eslint-disable-next-line complexity // eslint-disable-next-line complexity
.then(async (data) => { .then(async (data) => {
if (data === undefined) { if (data === undefined) {
if (!didSendResponse) { responsePromise.resolve({
responseCallback(null, { response: {
data: { body: {
message: 'Workflow executed successfully but no data was returned', message: 'Workflow executed successfully but no data was returned',
}, },
responseCode, statusCode: responseCodeParam,
headers: {},
},
}); });
didSendResponse = true; return;
}
return undefined;
} }
if (usePinData) { if (usePinData) {
@ -625,15 +595,15 @@ export async function executeWebhook(
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
if (data.data.resultData.error || returnData?.error !== undefined) { if (data.data.resultData.error || returnData?.error !== undefined) {
if (!didSendResponse) { responsePromise.resolve({
responseCallback(null, { response: {
data: { body: {
message: 'Error in workflow', message: 'Error in workflow',
}, },
responseCode: 500, statusCode: 500,
headers: {},
},
}); });
}
didSendResponse = true;
return data; return data;
} }
@ -644,16 +614,15 @@ export async function executeWebhook(
} }
if (returnData === undefined) { if (returnData === undefined) {
if (!didSendResponse) { responsePromise.resolve({
responseCallback(null, { response: {
data: { body: {
message: message: 'Workflow executed successfully but the last node did not return any data',
'Workflow executed successfully but the last node did not return any data', },
statusCode: responseCodeParam,
headers: {},
}, },
responseCode,
}); });
}
didSendResponse = true;
return data; return data;
} }
@ -661,19 +630,17 @@ export async function executeWebhook(
$executionId: executionId, $executionId: executionId,
}; };
if (!didSendResponse) { let responseData: IDataObject | IDataObject[] | undefined;
let data: IDataObject | IDataObject[] | undefined;
if (responseData === 'firstEntryJson') { if (responseDataParam === 'firstEntryJson') {
// Return the JSON data of the first entry // Return the JSON data of the first entry
if (returnData.data!.main[0]![0] === undefined) { if (returnData.data!.main[0]![0] === undefined) {
responseCallback(new ApplicationError('No item to return got found'), {}); responsePromise.reject(new ApplicationError('No item to return got found'));
didSendResponse = true; return;
return undefined;
} }
data = returnData.data!.main[0]![0].json; responseData = returnData.data!.main[0]![0].json;
const responsePropertyName = workflow.expression.getSimpleParameterValue( const responsePropertyName = workflow.expression.getSimpleParameterValue(
workflowStartNode, workflowStartNode,
@ -685,7 +652,7 @@ export async function executeWebhook(
); );
if (responsePropertyName !== undefined) { if (responsePropertyName !== undefined) {
data = get(data, responsePropertyName as string) as IDataObject; responseData = get(responseData, responsePropertyName as string) as IDataObject;
} }
const responseContentType = workflow.expression.getSimpleParameterValue( const responseContentType = workflow.expression.getSimpleParameterValue(
@ -703,34 +670,30 @@ export async function executeWebhook(
// Returning an object, boolean, number, ... causes problems so make sure to stringify if needed // Returning an object, boolean, number, ... causes problems so make sure to stringify if needed
if ( if (
data !== null && responseData !== null &&
data !== undefined && responseData !== undefined &&
['Buffer', 'String'].includes(data.constructor.name) ['Buffer', 'String'].includes(responseData.constructor.name)
) { ) {
res.end(data); res.end(responseData);
} else { } else {
res.end(JSON.stringify(data)); res.end(JSON.stringify(responseData));
} }
responseCallback(null, { responsePromise.resolve({ noWebhookResponse: true });
noWebhookResponse: true, return;
});
didSendResponse = true;
} }
} else if (responseData === 'firstEntryBinary') { } else if (responseDataParam === 'firstEntryBinary') {
// Return the binary data of the first entry // Return the binary data of the first entry
data = returnData.data!.main[0]![0]; responseData = returnData.data!.main[0]![0];
if (data === undefined) { if (responseData === undefined) {
responseCallback(new ApplicationError('No item was found to return'), {}); responsePromise.reject(new ApplicationError('No item was found to return'));
didSendResponse = true; return;
return undefined;
} }
if (data.binary === undefined) { if (responseData.binary === undefined) {
responseCallback(new ApplicationError('No binary data was found to return'), {}); responsePromise.reject(new ApplicationError('No binary data was found to return'));
didSendResponse = true; return;
return undefined;
} }
const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue( const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(
@ -742,28 +705,23 @@ export async function executeWebhook(
'data', 'data',
); );
if (responseBinaryPropertyName === undefined && !didSendResponse) { if (responseBinaryPropertyName === undefined) {
responseCallback( responsePromise.reject(new ApplicationError("No 'responseBinaryPropertyName' is set"));
new ApplicationError("No 'responseBinaryPropertyName' is set"), return;
{},
);
didSendResponse = true;
} }
const binaryData = (data.binary as IBinaryKeyData)[ const binaryData = (responseData.binary as IBinaryKeyData)[
responseBinaryPropertyName as string responseBinaryPropertyName as string
]; ];
if (binaryData === undefined && !didSendResponse) { if (binaryData === undefined) {
responseCallback( responsePromise.reject(
new ApplicationError( new ApplicationError(
`The binary property '${responseBinaryPropertyName}' which should be returned does not exist`, `The binary property '${responseBinaryPropertyName}' which should be returned does not exist`,
), ),
{},
); );
didSendResponse = true; return;
} }
if (!didSendResponse) {
// Send the webhook response manually // Send the webhook response manually
res.setHeader('Content-Type', binaryData.mimeType); res.setHeader('Content-Type', binaryData.mimeType);
if (binaryData.id) { if (binaryData.id) {
@ -774,49 +732,41 @@ export async function executeWebhook(
res.write(Buffer.from(binaryData.data, BINARY_ENCODING)); res.write(Buffer.from(binaryData.data, BINARY_ENCODING));
} }
responseCallback(null, { responsePromise.resolve({ noWebhookResponse: true });
noWebhookResponse: true,
});
process.nextTick(() => res.end()); process.nextTick(() => res.end());
} } else if (responseDataParam === 'noData') {
} else if (responseData === 'noData') {
// Return without data // Return without data
data = undefined; responseData = undefined;
} else { } else {
// Return the JSON data of all the entries // Return the JSON data of all the entries
data = []; responseData = [];
for (const entry of returnData.data!.main[0]!) { for (const entry of returnData.data!.main[0]!) {
data.push(entry.json); responseData.push(entry.json);
} }
} }
if (!didSendResponse) { responsePromise.resolve({
responseCallback(null, { response: {
data, body: responseData,
responseCode, statusCode: responseCodeParam,
headers: {},
},
}); });
}
}
didSendResponse = true;
return data; return data;
}) })
.catch((e) => { .catch((e) => {
if (!didSendResponse) { responsePromise.reject(
responseCallback(
new ApplicationError('There was a problem executing the workflow', { new ApplicationError('There was a problem executing the workflow', {
level: 'warning', level: 'warning',
cause: e, cause: e,
}), }),
{},
); );
}
const internalServerError = new InternalServerError(e.message, e); const internalServerError = new InternalServerError(e.message, e);
if (e instanceof ExecutionCancelledError) internalServerError.level = 'warning'; if (e instanceof ExecutionCancelledError) internalServerError.level = 'warning';
throw internalServerError; throw internalServerError;
}); });
}
return executionId; return executionId;
} catch (e) { } catch (e) {
const error = const error =
@ -826,8 +776,7 @@ export async function executeWebhook(
level: 'warning', level: 'warning',
cause: e, cause: e,
}); });
if (didSendResponse) throw error; responsePromise.reject(error);
responseCallback(error, {});
return; return;
} }
} }

View file

@ -42,18 +42,7 @@ class WebhookRequestHandler {
} }
try { try {
const response = await this.webhookManager.executeWebhook(req, res); 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,
);
}
} catch (e) { } catch (e) {
const error = ensureError(e); const error = ensureError(e);
Container.get(Logger).debug( Container.get(Logger).debug(

View file

@ -1,5 +1,5 @@
import type { Request, Response } from 'express'; 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' }; export type WebhookOptionsRequest = Request & { method: 'OPTIONS' };
@ -26,12 +26,12 @@ export interface IWebhookManager {
httpMethod: IHttpRequestMethods, httpMethod: IHttpRequestMethods,
) => Promise<WebhookAccessControlOptions | undefined>; ) => Promise<WebhookAccessControlOptions | undefined>;
executeWebhook(req: WebhookRequest, res: Response): Promise<IWebhookResponseCallbackData>; executeWebhook(req: WebhookRequest, res: Response): Promise<void>;
} }
export interface IWebhookResponseCallbackData { export type IWebhookResponsePromiseData =
data?: IDataObject | IDataObject[]; | { noWebhookResponse: true }
headers?: object; | {
noWebhookResponse?: boolean; noWebhookResponse?: false;
responseCode?: number; response: IN8nHttpFullResponse;
} };

View file

@ -1,6 +1,5 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { agent as testAgent } from 'supertest'; import { agent as testAgent } from 'supertest';
import type SuperAgentTest from 'supertest/lib/agent'; import type SuperAgentTest from 'supertest/lib/agent';
@ -10,7 +9,6 @@ import { TestWebhooks } from '@/webhooks/test-webhooks';
import { WaitingForms } from '@/webhooks/waiting-forms'; import { WaitingForms } from '@/webhooks/waiting-forms';
import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; import { WaitingWebhooks } from '@/webhooks/waiting-webhooks';
import { WebhookServer } from '@/webhooks/webhook-server'; import { WebhookServer } from '@/webhooks/webhook-server';
import type { IWebhookResponseCallbackData } from '@/webhooks/webhook.types';
import { mockInstance } from '@test/mocking'; import { mockInstance } from '@test/mocking';
let agent: SuperAgentTest; let agent: SuperAgentTest;
@ -60,9 +58,6 @@ describe('WebhookServer', () => {
it('should handle regular requests', async () => { it('should handle regular requests', async () => {
const pathPrefix = Container.get(GlobalConfig).endpoints[key]; const pathPrefix = Container.get(GlobalConfig).endpoints[key];
manager.getWebhookMethods.mockResolvedValueOnce(['GET']); manager.getWebhookMethods.mockResolvedValueOnce(['GET']);
manager.executeWebhook.mockResolvedValueOnce(
mockResponse({ test: true }, { key: 'value ' }),
);
const response = await agent const response = await agent
.get(`/${pathPrefix}/abcd`) .get(`/${pathPrefix}/abcd`)
@ -75,13 +70,5 @@ describe('WebhookServer', () => {
}); });
}); });
} }
const mockResponse = (data = {}, headers = {}, status = 200) => {
const response = mock<IWebhookResponseCallbackData>();
response.responseCode = status;
response.data = data;
response.headers = headers;
return response;
};
}); });
}); });

View file

@ -23,6 +23,12 @@ import type { Readable } from 'stream';
import { formatPrivateKey, generatePairedItemData } from '../../utils/utilities'; import { formatPrivateKey, generatePairedItemData } from '../../utils/utilities';
type Options = {
responseHeaders: { entries: Array<{ name: string; value: string }> };
responseCode: number;
responseKey: string;
};
export class RespondToWebhook implements INodeType { export class RespondToWebhook implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Respond to Webhook', displayName: 'Respond to Webhook',
@ -323,19 +329,16 @@ export class RespondToWebhook implements INodeType {
} }
const respondWith = this.getNodeParameter('respondWith', 0) as string; 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; const headers: IN8nHttpFullResponse['headers'] = {};
if (options.responseHeaders) { if (options.responseHeaders?.entries?.length) {
for (const header of (options.responseHeaders as IDataObject).entries as IDataObject[]) { for (const header of options.responseHeaders.entries) {
if (typeof header.name !== 'string') { headers[String(header.name).toLowerCase()] = String(header.value);
header.name = header.name?.toString();
}
headers[header.name?.toLowerCase() as string] = header.value?.toString();
} }
} }
let statusCode = (options.responseCode as number) || 200; let statusCode = options.responseCode ?? 200;
let responseBody: IN8nHttpResponse | Readable; let responseBody: IN8nHttpResponse | Readable;
if (respondWith === 'json') { if (respondWith === 'json') {
const responseBodyParameter = this.getNodeParameter('responseBody', 0) as string; const responseBodyParameter = this.getNodeParameter('responseBody', 0) as string;
@ -381,11 +384,11 @@ export class RespondToWebhook implements INodeType {
} else if (respondWith === 'allIncomingItems') { } else if (respondWith === 'allIncomingItems') {
const respondItems = items.map((item) => item.json); const respondItems = items.map((item) => item.json);
responseBody = options.responseKey responseBody = options.responseKey
? set({}, options.responseKey as string, respondItems) ? set({}, options.responseKey, respondItems)
: respondItems; : respondItems;
} else if (respondWith === 'firstIncomingItem') { } else if (respondWith === 'firstIncomingItem') {
responseBody = options.responseKey responseBody = options.responseKey
? set({}, options.responseKey as string, items[0].json) ? set({}, options.responseKey, items[0].json)
: items[0].json; : items[0].json;
} else if (respondWith === 'text') { } else if (respondWith === 'text') {
responseBody = this.getNodeParameter('responseBody', 0) as string; responseBody = this.getNodeParameter('responseBody', 0) as string;
@ -418,7 +421,7 @@ export class RespondToWebhook implements INodeType {
responseBody = { binaryData }; responseBody = { binaryData };
} else { } else {
responseBody = Buffer.from(binaryData.data, BINARY_ENCODING); 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']) { if (!headers['content-type']) {
@ -426,7 +429,7 @@ export class RespondToWebhook implements INodeType {
} }
} else if (respondWith === 'redirect') { } else if (respondWith === 'redirect') {
headers.location = this.getNodeParameter('redirectURL', 0) as string; headers.location = this.getNodeParameter('redirectURL', 0) as string;
statusCode = (options.responseCode as number) ?? 307; statusCode = options.responseCode ?? 307;
} else if (respondWith !== 'noData') { } else if (respondWith !== 'noData') {
throw new NodeOperationError( throw new NodeOperationError(
this.getNode(), this.getNode(),

View file

@ -518,10 +518,12 @@ export interface PaginationOptions {
export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[] | null; export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[] | null;
type ResponseHeaders = Record<string, string>;
export interface IN8nHttpFullResponse { export interface IN8nHttpFullResponse {
body: IN8nHttpResponse | Readable; body: IN8nHttpResponse | Readable;
__bodyResolved?: boolean; __bodyResolved?: boolean;
headers: IDataObject; headers: ResponseHeaders;
statusCode: number; statusCode: number;
statusMessage?: string; statusMessage?: string;
} }