mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(Webhook Trigger Node)!: Change html responses to be embedded an iframe (#17312)
Co-authored-by: Dana Lee <dana@n8n.io>
This commit is contained in:
@@ -2,6 +2,16 @@
|
||||
|
||||
This list shows all the versions which include breaking changes and how to upgrade.
|
||||
|
||||
## 1.103.0
|
||||
|
||||
### What changed?
|
||||
|
||||
We will no longer be allowing users to use `responseData` within the Webhook node since this is now sandboxed in an iframe, which may break workflows relying on browser APIs like `localStorage` and `fetch` from within custom code.
|
||||
|
||||
### When is action necessary?
|
||||
|
||||
If your workflow is using the Webhook node and uses JavaScript in `responseData` to make `fetch` calls or access `localStorage`, you may need to refactor it due to the new iframe sandboxing.
|
||||
|
||||
## 1.102.0
|
||||
|
||||
### What changed?
|
||||
@@ -12,8 +22,8 @@ with libraries like `puppeteer` at the cost of security.
|
||||
|
||||
### When is action necessary?
|
||||
|
||||
If you are using the `N8N_RUNNERS_ALLOW_PROTOTYPE_MUTATION` flag, or if you find that the task runner does not
|
||||
currently support an external module that you rely on, then consider setting `N8N_RUNNERS_INSECURE_MODE=true`,
|
||||
If you are using the `N8N_RUNNERS_ALLOW_PROTOTYPE_MUTATION` flag, or if you find that the task runner does not
|
||||
currently support an external module that you rely on, then consider setting `N8N_RUNNERS_INSECURE_MODE=true`,
|
||||
at your own risk.
|
||||
|
||||
## 1.98.0
|
||||
|
||||
@@ -21,7 +21,6 @@ import { finished } from 'stream/promises';
|
||||
import {
|
||||
autoDetectResponseMode,
|
||||
handleFormRedirectionCase,
|
||||
getResponseOnReceived,
|
||||
setupResponseNodePromise,
|
||||
prepareExecutionData,
|
||||
} from '../webhook-helpers';
|
||||
@@ -140,49 +139,6 @@ describe('handleFormRedirectionCase', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResponseOnReceived', () => {
|
||||
const responseCode = 200;
|
||||
const webhookResultData = mock<IWebhookResponseData>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should return response with no data when responseData is "noData"', () => {
|
||||
const callbackData = getResponseOnReceived('noData', webhookResultData, responseCode);
|
||||
|
||||
expect(callbackData).toEqual({ responseCode });
|
||||
});
|
||||
|
||||
test('should return response with responseData when it is defined', () => {
|
||||
const responseData = JSON.stringify({ foo: 'bar' });
|
||||
|
||||
const callbackData = getResponseOnReceived(responseData, webhookResultData, responseCode);
|
||||
|
||||
expect(callbackData).toEqual({ data: responseData, responseCode });
|
||||
});
|
||||
|
||||
test('should return response with webhookResponse when responseData is falsy but webhookResponse exists', () => {
|
||||
const webhookResponse = { success: true };
|
||||
webhookResultData.webhookResponse = webhookResponse;
|
||||
|
||||
const callbackData = getResponseOnReceived(undefined, webhookResultData, responseCode);
|
||||
|
||||
expect(callbackData).toEqual({ data: webhookResponse, responseCode });
|
||||
});
|
||||
|
||||
test('should return default response message when responseData and webhookResponse are falsy', () => {
|
||||
webhookResultData.webhookResponse = undefined;
|
||||
|
||||
const callbackData = getResponseOnReceived(undefined, webhookResultData, responseCode);
|
||||
|
||||
expect(callbackData).toEqual({
|
||||
data: { message: 'Workflow was started' },
|
||||
responseCode,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupResponseNodePromise', () => {
|
||||
const workflowId = 'test-workflow-id';
|
||||
const executionId = 'test-execution-id';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IWebhookResponseData } from 'n8n-workflow';
|
||||
|
||||
import { extractWebhookOnReceivedResponse } from '@/webhooks/webhook-on-received-response-extractor';
|
||||
|
||||
describe('extractWebhookOnReceivedResponse', () => {
|
||||
const webhookResultData = mock<IWebhookResponseData>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should return response with no data when responseData is "noData"', () => {
|
||||
const callbackData = extractWebhookOnReceivedResponse('noData', webhookResultData);
|
||||
|
||||
expect(callbackData).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('should return response with responseData when it is defined', () => {
|
||||
const responseData = JSON.stringify({ foo: 'bar' });
|
||||
|
||||
const callbackData = extractWebhookOnReceivedResponse(responseData, webhookResultData);
|
||||
|
||||
expect(callbackData).toEqual(responseData);
|
||||
});
|
||||
|
||||
test('should return response with webhookResponse when responseData is falsy but webhookResponse exists', () => {
|
||||
const webhookResponse = { success: true };
|
||||
webhookResultData.webhookResponse = webhookResponse;
|
||||
|
||||
const callbackData = extractWebhookOnReceivedResponse(undefined, webhookResultData);
|
||||
|
||||
expect(callbackData).toEqual(webhookResponse);
|
||||
});
|
||||
|
||||
test('should return default response message when responseData and webhookResponse are falsy', () => {
|
||||
webhookResultData.webhookResponse = undefined;
|
||||
|
||||
const callbackData = extractWebhookOnReceivedResponse(undefined, webhookResultData);
|
||||
|
||||
expect(callbackData).toEqual({ message: 'Workflow was started' });
|
||||
});
|
||||
});
|
||||
@@ -63,6 +63,7 @@ import { WaitTracker } from '@/wait-tracker';
|
||||
import { WebhookExecutionContext } from '@/webhooks/webhook-execution-context';
|
||||
import { createMultiFormDataParser } from '@/webhooks/webhook-form-data';
|
||||
import { extractWebhookLastNodeResponse } from '@/webhooks/webhook-last-node-response-extractor';
|
||||
import { extractWebhookOnReceivedResponse } from '@/webhooks/webhook-on-received-response-extractor';
|
||||
import type { WebhookResponse } from '@/webhooks/webhook-response';
|
||||
import { createStaticResponse, createStreamResponse } from '@/webhooks/webhook-response';
|
||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||
@@ -205,28 +206,6 @@ export const handleFormRedirectionCase = (
|
||||
const { formDataFileSizeMax } = Container.get(GlobalConfig).endpoints;
|
||||
const parseFormData = createMultiFormDataParser(formDataFileSizeMax);
|
||||
|
||||
/** Return webhook response when responseMode is set to "onReceived" */
|
||||
export function getResponseOnReceived(
|
||||
responseData: WebhookResponseData | string | undefined,
|
||||
webhookResultData: IWebhookResponseData,
|
||||
responseCode: number,
|
||||
): IWebhookResponseCallbackData {
|
||||
const callbackData: IWebhookResponseCallbackData = { responseCode };
|
||||
// Return response directly and do not wait for the workflow to finish
|
||||
if (responseData === 'noData') {
|
||||
// Return without data
|
||||
} else if (responseData) {
|
||||
// Return the data specified in the response data option
|
||||
callbackData.data = responseData as unknown as IDataObject;
|
||||
} else if (webhookResultData.webhookResponse !== undefined) {
|
||||
// Data to respond with is given
|
||||
callbackData.data = webhookResultData.webhookResponse;
|
||||
} else {
|
||||
callbackData.data = { message: 'Workflow was started' };
|
||||
}
|
||||
return callbackData;
|
||||
}
|
||||
|
||||
export function setupResponseNodePromise(
|
||||
responsePromise: IDeferredPromise<IN8nHttpFullResponse>,
|
||||
res: express.Response,
|
||||
@@ -549,8 +528,9 @@ export async function executeWebhook(
|
||||
// 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) {
|
||||
const callbackData = getResponseOnReceived(responseData, webhookResultData, responseCode);
|
||||
responseCallback(null, callbackData);
|
||||
const responseBody = extractWebhookOnReceivedResponse(responseData, webhookResultData);
|
||||
const webhookResponse = createStaticResponse(responseBody, responseCode, responseHeaders);
|
||||
responseCallback(null, webhookResponse);
|
||||
didSendResponse = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { IWebhookResponseData, WebhookResponseData } from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
+ * Creates the response for a webhook when the response mode is set to
|
||||
* `onReceived`.
|
||||
*
|
||||
* @param context - The webhook execution context
|
||||
* @param responseData - The evaluated `responseData` option of the webhook node
|
||||
* @param webhookResultData - The webhook result data that the webhook might have returned when it was ran
|
||||
*
|
||||
* @returns The response body
|
||||
*/
|
||||
export function extractWebhookOnReceivedResponse(
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
responseData: Extract<WebhookResponseData, 'noData'> | string | undefined,
|
||||
webhookResultData: IWebhookResponseData,
|
||||
): unknown {
|
||||
// Return response directly and do not wait for the workflow to finish
|
||||
if (responseData === 'noData') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (responseData) {
|
||||
return responseData;
|
||||
}
|
||||
|
||||
if (webhookResultData.webhookResponse !== undefined) {
|
||||
// Data to respond with is given
|
||||
return webhookResultData.webhookResponse as unknown;
|
||||
}
|
||||
|
||||
return { message: 'Workflow was started' };
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { Container } from '@n8n/di';
|
||||
import type express from 'express';
|
||||
import {
|
||||
isHtmlRenderedContentType,
|
||||
sandboxHtmlResponse,
|
||||
createHtmlSandboxTransformStream,
|
||||
} from 'n8n-core';
|
||||
import { ensureError, type IHttpRequestMethods } from 'n8n-workflow';
|
||||
import { finished } from 'stream/promises';
|
||||
|
||||
@@ -121,8 +126,13 @@ class WebhookRequestHandler {
|
||||
this.setResponseStatus(res, code);
|
||||
this.setResponseHeaders(res, headers);
|
||||
|
||||
stream.pipe(res, { end: false });
|
||||
await finished(stream);
|
||||
const contentType = res.getHeader('content-type') as string | undefined;
|
||||
const needsSandbox = contentType && isHtmlRenderedContentType(contentType);
|
||||
|
||||
const streamToSend = needsSandbox ? stream.pipe(createHtmlSandboxTransformStream()) : stream;
|
||||
streamToSend.pipe(res, { end: false });
|
||||
await finished(streamToSend);
|
||||
|
||||
process.nextTick(() => res.end());
|
||||
}
|
||||
|
||||
@@ -132,10 +142,19 @@ class WebhookRequestHandler {
|
||||
this.setResponseStatus(res, code);
|
||||
this.setResponseHeaders(res, headers);
|
||||
|
||||
const contentType = res.getHeader('content-type') as string | undefined;
|
||||
|
||||
if (typeof body === 'string') {
|
||||
res.send(body);
|
||||
const needsSandbox = !contentType || isHtmlRenderedContentType(contentType);
|
||||
const bodyToSend = needsSandbox ? sandboxHtmlResponse(body) : body;
|
||||
res.send(bodyToSend);
|
||||
} else {
|
||||
res.json(body);
|
||||
const needsSandbox = contentType && isHtmlRenderedContentType(contentType);
|
||||
if (needsSandbox) {
|
||||
res.send(sandboxHtmlResponse(JSON.stringify(body)));
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user