Add DELETE, PATCH and PUT request support to Webhooks

This commit is contained in:
Jan Oberhauser 2022-02-20 10:30:01 +01:00
parent ec5bfaf895
commit 9a06d0fffc
5 changed files with 103 additions and 249 deletions

View file

@ -72,6 +72,7 @@ import {
IWorkflowBase,
LoggerProxy,
NodeHelpers,
WebhookHttpMethod,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
@ -202,6 +203,8 @@ class App {
presetCredentialsLoaded: boolean;
webhookMethods: WebhookHttpMethod[];
constructor() {
this.app = express();
@ -237,6 +240,8 @@ class App {
this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string;
this.webhookMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'];
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
const telemetrySettings: ITelemetrySettings = {
@ -2704,8 +2709,8 @@ class App {
WebhookServer.registerProductionWebhooks.apply(this);
}
// HEAD webhook requests (test for UI)
this.app.head(
// Register all webhook requests (test for UI)
this.app.all(
`/${this.endpointWebhookTest}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-test/" to get the registred part of the url
@ -2713,98 +2718,36 @@ class App {
this.endpointWebhookTest.length + 2,
);
const method = req.method.toUpperCase() as WebhookHttpMethod;
if (method === 'OPTIONS') {
let allowedMethods: string[];
try {
allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl);
allowedMethods.push('OPTIONS');
// Add custom "Allow" header to satisfy OPTIONS response.
res.append('Allow', allowedMethods);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
return;
}
if (!this.webhookMethods.includes(method)) {
ResponseHelper.sendErrorResponse(
res,
new Error(`The method ${method} is not supported.`),
);
return;
}
let response;
try {
response = await this.testWebhooks.callTestWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
// HEAD webhook requests (test for UI)
this.app.options(
`/${this.endpointWebhookTest}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-test/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookTest.length + 2,
);
let allowedMethods: string[];
try {
allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl);
allowedMethods.push('OPTIONS');
// Add custom "Allow" header to satisfy OPTIONS response.
res.append('Allow', allowedMethods);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
},
);
// GET webhook requests (test for UI)
this.app.get(
`/${this.endpointWebhookTest}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-test/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookTest.length + 2,
);
let response;
try {
response = await this.testWebhooks.callTestWebhook('GET', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
// POST webhook requests (test for UI)
this.app.post(
`/${this.endpointWebhookTest}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-test/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookTest.length + 2,
);
let response;
try {
response = await this.testWebhooks.callTestWebhook('POST', requestUrl, req, res);
response = await this.testWebhooks.callTestWebhook(method, requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;

View file

@ -16,6 +16,7 @@ import * as _ from 'lodash';
import * as compression from 'compression';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as parseUrl from 'parseurl';
import { WebhookHttpMethod } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import {
ActiveExecutions,
@ -41,8 +42,8 @@ export function registerProductionWebhooks() {
// Regular Webhooks
// ----------------------------------------
// HEAD webhook requests
this.app.head(
// Register all webhook requests
this.app.all(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
@ -50,99 +51,34 @@ export function registerProductionWebhooks() {
this.endpointWebhook.length + 2,
);
const method = req.method.toUpperCase() as WebhookHttpMethod;
if (method === 'OPTIONS') {
let allowedMethods: string[];
try {
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
allowedMethods.push('OPTIONS');
// Add custom "Allow" header to satisfy OPTIONS response.
res.append('Allow', allowedMethods);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
return;
}
if (!this.webhookMethods.includes(method)) {
ResponseHelper.sendErrorResponse(res, new Error(`The method ${method} is not supported.`));
return;
}
let response;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
// OPTIONS webhook requests
this.app.options(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let allowedMethods: string[];
try {
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
allowedMethods.push('OPTIONS');
// Add custom "Allow" header to satisfy OPTIONS response.
res.append('Allow', allowedMethods);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
},
);
// GET webhook requests
this.app.get(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
// POST webhook requests
this.app.post(
`/${this.endpointWebhook}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhook.length + 2,
);
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('POST', requestUrl, req, res);
response = await this.activeWorkflowRunner.executeWebhook(method, requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
@ -169,8 +105,8 @@ export function registerProductionWebhooks() {
const waitingWebhooks = new WaitingWebhooks();
// HEAD webhook-waiting requests
this.app.head(
// Register all webhook-waiting requests
this.app.all(
`/${this.endpointWebhookWaiting}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
@ -178,73 +114,20 @@ export function registerProductionWebhooks() {
this.endpointWebhookWaiting.length + 2,
);
let response;
try {
response = await waitingWebhooks.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
const method = req.method.toUpperCase() as WebhookHttpMethod;
// TOOD: Add support for OPTIONS in the future
// if (method === 'OPTIONS') {
// }
if (!this.webhookMethods.includes(method)) {
ResponseHelper.sendErrorResponse(res, new Error(`The method ${method} is not supported.`));
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
// GET webhook-waiting requests
this.app.get(
`/${this.endpointWebhookWaiting}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookWaiting.length + 2,
);
let response;
try {
response = await waitingWebhooks.executeWebhook('GET', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(
res,
response.data,
true,
response.responseCode,
response.headers,
);
},
);
// POST webhook-waiting requests
this.app.post(
`/${this.endpointWebhookWaiting}/*`,
async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-waiting/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
this.endpointWebhookWaiting.length + 2,
);
let response;
try {
response = await waitingWebhooks.executeWebhook('POST', requestUrl, req, res);
response = await waitingWebhooks.executeWebhook(method, requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;

View file

@ -250,6 +250,10 @@ export class Wait implements INodeType {
},
},
options: [
{
name: 'DELETE',
value: 'DELETE',
},
{
name: 'GET',
value: 'GET',
@ -258,10 +262,18 @@ export class Wait implements INodeType {
name: 'HEAD',
value: 'HEAD',
},
{
name: 'PATCH',
value: 'PATCH',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'PUT',
value: 'PUT',
},
],
default: 'GET',
description: 'The HTTP method of the Webhook call',
@ -514,6 +526,8 @@ export class Wait implements INodeType {
displayOptions: {
show: {
'/httpMethod': [
'PATCH',
'PUT',
'POST',
],
},

View file

@ -120,6 +120,10 @@ export class Webhook implements INodeType {
name: 'httpMethod',
type: 'options',
options: [
{
name: 'DELETE',
value: 'DELETE',
},
{
name: 'GET',
value: 'GET',
@ -128,10 +132,18 @@ export class Webhook implements INodeType {
name: 'HEAD',
value: 'HEAD',
},
{
name: 'PATCH',
value: 'PATCH',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'PUT',
value: 'PUT',
},
],
default: 'GET',
description: 'The HTTP method to listen to.',
@ -265,6 +277,8 @@ export class Webhook implements INodeType {
displayOptions: {
show: {
'/httpMethod': [
'PATCH',
'PUT',
'POST',
],
},

View file

@ -1178,7 +1178,7 @@ export interface IWorkflowMetadata {
active: boolean;
}
export type WebhookHttpMethod = 'GET' | 'POST' | 'HEAD' | 'OPTIONS';
export type WebhookHttpMethod = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS';
export interface IWebhookResponseData {
workflowData?: INodeExecutionData[][];