refactor(core): Parse Webhook request bodies on-demand (#6394)

Also,
1. Consistent CORS support ~on all three webhook types~ waiting webhooks never supported CORS. I'll fix that in another PR
2. [Fixes binary-data handling when request body is text, json, or xml](https://linear.app/n8n/issue/NODE-505/webhook-binary-data-handling-fails-for-textplain-files).
3. Reduced number of middleware that each request has to go through.
4. Removed the need to maintain webhook endpoints in the auth-exception list.
5. Skip all middlewares (apart from `compression`) on Webhook routes. 
6. move `multipart/form-data` support out of individual nodes
7. upgrade `formidable`
8. fix the filenames on binary-data in webhooks nodes
9. add unit tests and integration tests for webhook request handling, and increase test coverage
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-08-01 17:32:30 +02:00
committed by GitHub
parent 369a2e9796
commit 31d8f478ee
29 changed files with 905 additions and 604 deletions

View File

@@ -1,5 +1,3 @@
import * as formidable from 'formidable';
import type {
IHookFunctions,
IWebhookFunctions,
@@ -9,6 +7,7 @@ import type {
INodeType,
INodeTypeDescription,
IWebhookResponseData,
MultiPartFormData,
} from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
@@ -132,7 +131,6 @@ export class JotFormTrigger implements INodeType {
const endpoint = `/form/${formId}/webhooks`;
const body: IDataObject = {
webhookURL: webhookUrl,
//webhookURL: 'https://en0xsizp3qyt7f.x.pipedream.net/',
};
const { content } = await jotformApiRequest.call(this, 'POST', endpoint, body);
webhookData.webhookId = Object.keys(content as IDataObject)[0];
@@ -158,71 +156,65 @@ export class JotFormTrigger implements INodeType {
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
const req = this.getRequestObject() as MultiPartFormData.Request;
const formId = this.getNodeParameter('form') as string;
const resolveData = this.getNodeParameter('resolveData', false) as boolean;
const onlyAnswers = this.getNodeParameter('onlyAnswers', false) as boolean;
const form = new formidable.IncomingForm({});
const { data } = req.body;
return new Promise((resolve, _reject) => {
form.parse(req, async (err, data, _files) => {
const rawRequest = jsonParse<any>(data.rawRequest as string);
data.rawRequest = rawRequest;
const rawRequest = jsonParse<any>(data.rawRequest as string);
data.rawRequest = rawRequest;
let returnData: IDataObject;
if (!resolveData) {
if (onlyAnswers) {
returnData = data.rawRequest as unknown as IDataObject;
} else {
returnData = data;
}
let returnData: IDataObject;
if (!resolveData) {
if (onlyAnswers) {
returnData = data.rawRequest as unknown as IDataObject;
} else {
returnData = data;
}
resolve({
workflowData: [this.helpers.returnJsonArray(returnData)],
});
}
return {
workflowData: [this.helpers.returnJsonArray(returnData)],
};
}
// Resolve the data by requesting the information via API
const endpoint = `/form/${formId}/questions`;
const responseData = await jotformApiRequest.call(this, 'GET', endpoint, {});
// Resolve the data by requesting the information via API
const endpoint = `/form/${formId}/questions`;
const responseData = await jotformApiRequest.call(this, 'GET', endpoint, {});
// Create a dictionary to resolve the keys
const questionNames: IDataObject = {};
for (const question of Object.values<IQuestionData>(
responseData.content as IQuestionData[],
)) {
questionNames[question.name] = question.text;
}
// Create a dictionary to resolve the keys
const questionNames: IDataObject = {};
for (const question of Object.values<IQuestionData>(responseData.content as IQuestionData[])) {
questionNames[question.name] = question.text;
}
// Resolve the keys
let questionKey: string;
const questionsData: IDataObject = {};
for (const key of Object.keys(rawRequest as IDataObject)) {
if (!key.includes('_')) {
continue;
}
// Resolve the keys
let questionKey: string;
const questionsData: IDataObject = {};
for (const key of Object.keys(rawRequest as IDataObject)) {
if (!key.includes('_')) {
continue;
}
questionKey = key.split('_').slice(1).join('_');
if (questionNames[questionKey] === undefined) {
continue;
}
questionKey = key.split('_').slice(1).join('_');
if (questionNames[questionKey] === undefined) {
continue;
}
questionsData[questionNames[questionKey] as string] = rawRequest[key];
}
questionsData[questionNames[questionKey] as string] = rawRequest[key];
}
if (onlyAnswers) {
returnData = questionsData as unknown as IDataObject;
} else {
// @ts-ignore
data.rawRequest = questionsData;
returnData = data;
}
if (onlyAnswers) {
returnData = questionsData as unknown as IDataObject;
} else {
// @ts-ignore
data.rawRequest = questionsData;
returnData = data;
}
resolve({
workflowData: [this.helpers.returnJsonArray(returnData)],
});
});
});
return {
workflowData: [this.helpers.returnJsonArray(returnData)],
};
}
}