first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,16 @@
{
"node": "n8n-nodes-base.respondToWebhook",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Core Nodes", "Utility"],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/"
}
]
},
"subcategories": {
"Core Nodes": ["Helpers"]
}
}
@@ -0,0 +1,598 @@
import jwt from 'jsonwebtoken';
import set from 'lodash/set';
import type {
IDataObject,
IExecuteFunctions,
IN8nHttpFullResponse,
IN8nHttpResponse,
INodeExecutionData,
INodeProperties,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
jsonParse,
NodeOperationError,
NodeConnectionTypes,
WEBHOOK_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
WAIT_NODE_TYPE,
WAIT_INDEFINITELY,
} from 'n8n-workflow';
import type { Readable } from 'stream';
import { getBinaryResponse } from './utils/binary';
import { configuredOutputs } from './utils/outputs';
import { formatPrivateKey, generatePairedItemData } from '../../utils/utilities';
const respondWithProperty: INodeProperties = {
displayName: 'Respond With',
name: 'respondWith',
type: 'options',
options: [
{
name: 'All Incoming Items',
value: 'allIncomingItems',
description: 'Respond with all input JSON items',
},
{
name: 'Binary File',
value: 'binary',
description: 'Respond with incoming file binary data',
},
{
name: 'First Incoming Item',
value: 'firstIncomingItem',
description: 'Respond with the first input JSON item',
},
{
name: 'JSON',
value: 'json',
description: 'Respond with a custom JSON body',
},
{
name: 'JWT Token',
value: 'jwt',
description: 'Respond with a JWT token',
},
{
name: 'No Data',
value: 'noData',
description: 'Respond with an empty body',
},
{
name: 'Redirect',
value: 'redirect',
description: 'Respond with a redirect to a given URL',
},
{
name: 'Text',
value: 'text',
description: 'Respond with a simple text message body',
},
],
default: 'firstIncomingItem',
description: 'The data that should be returned',
};
export class RespondToWebhook implements INodeType {
description: INodeTypeDescription = {
displayName: 'Respond to Webhook',
icon: { light: 'file:webhook.svg', dark: 'file:webhook.dark.svg' },
name: 'respondToWebhook',
group: ['transform'],
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5],
defaultVersion: 1.5,
description: 'Returns data for Webhook',
defaults: {
name: 'Respond to Webhook',
},
builderHint: {
message:
'Only works with webhook node (n8n-nodes-base.webhook) with responseMode set to "responseNode"',
relatedNodes: [
{
nodeType: 'n8n-nodes-base.webhook',
relationHint: 'Required trigger - set responseMode to "responseNode"',
},
],
},
inputs: [NodeConnectionTypes.Main],
outputs: `={{(${configuredOutputs})($nodeVersion, $parameter)}}`,
credentials: [
{
name: 'jwtAuth',
required: true,
displayOptions: {
show: {
respondWith: ['jwt'],
},
},
},
],
properties: [
{
displayName: 'Enable Response Output Branch',
name: 'enableResponseOutput',
type: 'boolean',
default: false,
description:
'Whether to provide an additional output branch with the response sent to the webhook',
isNodeSetting: true,
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.4 } }] } },
},
{
displayName:
'Verify that the "Webhook" node\'s "Respond" parameter is set to "Using Respond to Webhook Node". <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/" target="_blank">More details',
name: 'generalNotice',
type: 'notice',
default: '',
},
{
...respondWithProperty,
displayOptions: { show: { '@version': [1, 1.1] } },
},
{
...respondWithProperty,
noDataExpression: true,
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.2 } }] } },
},
{
displayName: 'Credentials',
name: 'credentials',
type: 'credentials',
default: '',
displayOptions: {
show: {
respondWith: ['jwt'],
},
},
},
{
displayName:
'When using expressions, note that this node will only run for the first item in the input data',
name: 'webhookNotice',
type: 'notice',
displayOptions: {
show: {
respondWith: ['json', 'text', 'jwt'],
},
},
default: '',
},
{
displayName: 'Redirect URL',
name: 'redirectURL',
type: 'string',
required: true,
displayOptions: {
show: {
respondWith: ['redirect'],
},
},
default: '',
placeholder: 'e.g. http://www.n8n.io',
description: 'The URL to redirect to',
validateType: 'url',
},
{
displayName: 'Response Body',
name: 'responseBody',
type: 'json',
displayOptions: {
show: {
respondWith: ['json'],
},
},
default: '{\n "myField": "value"\n}',
typeOptions: {
rows: 4,
},
description: 'The HTTP response JSON data',
},
{
displayName: 'Payload',
name: 'payload',
type: 'json',
displayOptions: {
show: {
respondWith: ['jwt'],
},
},
default: '{\n "myField": "value"\n}',
typeOptions: {
rows: 4,
},
validateType: 'object',
description: 'The payload to include in the JWT token',
},
{
displayName: 'Response Body',
name: 'responseBody',
type: 'string',
displayOptions: {
show: {
respondWith: ['text'],
},
},
typeOptions: {
rows: 2,
},
default: '',
placeholder: 'e.g. Workflow completed',
description: 'The HTTP response text data',
},
{
displayName: 'Response Data Source',
name: 'responseDataSource',
type: 'options',
displayOptions: {
show: {
respondWith: ['binary'],
},
},
options: [
{
name: 'Choose Automatically From Input',
value: 'automatically',
description: 'Use if input data will contain a single piece of binary data',
},
{
name: 'Specify Myself',
value: 'set',
description: 'Enter the name of the input field the binary data will be in',
},
],
default: 'automatically',
},
{
displayName: 'Input Field Name',
name: 'inputFieldName',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
respondWith: ['binary'],
responseDataSource: ['set'],
},
},
description: 'The name of the node input field with the binary data',
},
{
displayName:
'To avoid unexpected behavior, add a "Content-Type" response header with the appropriate value',
name: 'contentTypeNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
respondWith: ['text'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Response Code',
name: 'responseCode',
type: 'number',
typeOptions: {
minValue: 100,
maxValue: 599,
},
default: 200,
description: 'The HTTP response code to return. Defaults to 200.',
},
{
displayName: 'Response Headers',
name: 'responseHeaders',
placeholder: 'Add Response Header',
description: 'Add headers to the webhook response',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'entries',
displayName: 'Entries',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the header',
},
],
},
],
},
{
displayName: 'Put Response in Field',
name: 'responseKey',
type: 'string',
displayOptions: {
show: {
['/respondWith']: ['allIncomingItems', 'firstIncomingItem'],
},
},
default: '',
description: 'The name of the response field to put all items in',
placeholder: 'e.g. data',
},
{
displayName: 'Enable Streaming',
name: 'enableStreaming',
type: 'boolean',
default: true,
description: 'Whether to enable streaming to the response',
displayOptions: {
show: {
['/respondWith']: ['allIncomingItems', 'firstIncomingItem', 'text', 'json', 'jwt'],
'@version': [{ _cnd: { gte: 1.5 } }],
},
},
},
],
},
],
};
async onMessage(
context: IExecuteFunctions,
_data: INodeExecutionData,
): Promise<INodeExecutionData[][]> {
const inputData = context.getInputData();
return [inputData];
}
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const nodeVersion = this.getNode().typeVersion;
const WEBHOOK_NODE_TYPES = [
WEBHOOK_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
WAIT_NODE_TYPE,
];
let response: IN8nHttpFullResponse;
const connectedNodes = this.getParentNodes(this.getNode().name, {
includeNodeParameters: true,
});
const options = this.getNodeParameter('options', 0, {});
const shouldStream =
nodeVersion >= 1.5 && this.isStreaming() && options.enableStreaming !== false;
try {
if (nodeVersion >= 1.1) {
if (!connectedNodes.some(({ type }) => WEBHOOK_NODE_TYPES.includes(type))) {
throw new NodeOperationError(
this.getNode(),
new Error('No Webhook node found in the workflow'),
{
description:
'Insert a Webhook node to your workflow and set the “Respond” parameter to “Using Respond to Webhook Node” ',
},
);
}
}
const respondWith = this.getNodeParameter('respondWith', 0) as string;
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();
}
}
let statusCode = (options.responseCode as number) || 200;
let responseBody: IN8nHttpResponse | Readable;
if (respondWith === 'json') {
const responseBodyParameter = this.getNodeParameter('responseBody', 0) as string;
if (responseBodyParameter) {
if (typeof responseBodyParameter === 'object') {
responseBody = responseBodyParameter;
} else {
try {
responseBody = jsonParse(responseBodyParameter);
} catch (error) {
throw new NodeOperationError(this.getNode(), error as Error, {
message: "Invalid JSON in 'Response Body' field",
description:
"Check that the syntax of the JSON in the 'Response Body' parameter is valid",
});
}
}
}
if (shouldStream) {
this.sendChunk('begin', 0);
this.sendChunk('item', 0, responseBody as IDataObject);
this.sendChunk('end', 0);
}
} else if (respondWith === 'jwt') {
try {
const { keyType, secret, algorithm, privateKey } = await this.getCredentials<{
keyType: 'passphrase' | 'pemKey';
privateKey: string;
secret: string;
algorithm: jwt.Algorithm;
}>('jwtAuth');
let secretOrPrivateKey;
if (keyType === 'passphrase') {
secretOrPrivateKey = secret;
} else {
secretOrPrivateKey = formatPrivateKey(privateKey);
}
const payload = this.getNodeParameter('payload', 0, {}) as IDataObject;
const token = jwt.sign(payload, secretOrPrivateKey, { algorithm });
responseBody = { token };
if (shouldStream) {
this.sendChunk('begin', 0);
this.sendChunk('item', 0, responseBody as IDataObject);
this.sendChunk('end', 0);
}
} catch (error) {
throw new NodeOperationError(this.getNode(), error as Error, {
message: 'Error signing JWT token',
});
}
} else if (respondWith === 'allIncomingItems') {
const respondItems = items.map((item, index) => {
this.sendChunk('begin', index);
this.sendChunk('item', index, item.json);
this.sendChunk('end', index);
return item.json;
});
responseBody = options.responseKey
? set({}, options.responseKey as string, respondItems)
: respondItems;
} else if (respondWith === 'firstIncomingItem') {
responseBody = options.responseKey
? set({}, options.responseKey as string, items[0].json)
: items[0].json;
if (shouldStream) {
this.sendChunk('begin', 0);
this.sendChunk('item', 0, items[0].json);
this.sendChunk('end', 0);
}
} else if (respondWith === 'text') {
const rawBody = this.getNodeParameter('responseBody', 0) as string;
responseBody = rawBody;
// Send the raw body to the stream
if (shouldStream) {
this.sendChunk('begin', 0);
this.sendChunk('item', 0, rawBody);
this.sendChunk('end', 0);
}
} else if (respondWith === 'binary') {
const item = items[0];
if (item.binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on the first item!');
}
let responseBinaryPropertyName: string;
const responseDataSource = this.getNodeParameter('responseDataSource', 0) as string;
if (responseDataSource === 'set') {
responseBinaryPropertyName = this.getNodeParameter('inputFieldName', 0) as string;
} else {
const binaryKeys = Object.keys(item.binary);
if (binaryKeys.length === 0) {
throw new NodeOperationError(
this.getNode(),
'No binary data exists on the first item!',
);
}
responseBinaryPropertyName = binaryKeys[0];
}
const binaryData = this.helpers.assertBinaryData(0, responseBinaryPropertyName);
responseBody = getBinaryResponse(binaryData, headers);
} else if (respondWith === 'redirect') {
headers.location = this.getNodeParameter('redirectURL', 0) as string;
statusCode = (options.responseCode as number) ?? 307;
} else if (respondWith !== 'noData') {
throw new NodeOperationError(
this.getNode(),
`The Response Data option "${respondWith}" is not supported!`,
);
}
const chatTrigger = connectedNodes.find(
(node) => node.type === CHAT_TRIGGER_NODE_TYPE && !node.disabled,
);
const parameters = chatTrigger?.parameters as {
options: { responseMode: string };
};
// if workflow is started from chat trigger and responseMode is set to "responseNodes"
// response to chat will be send by ChatService
if (
chatTrigger &&
!chatTrigger.disabled &&
parameters.options.responseMode === 'responseNodes'
) {
let message = '';
if (responseBody && typeof responseBody === 'object' && !Array.isArray(responseBody)) {
message =
(((responseBody as IDataObject).output ??
(responseBody as IDataObject).text ??
(responseBody as IDataObject).message) as string) ?? '';
if (message === '' && Object.keys(responseBody).length > 0) {
try {
message = JSON.stringify(responseBody, null, 2);
} catch (e) {}
}
}
await this.putExecutionToWait(WAIT_INDEFINITELY);
return [[{ json: {}, sendMessage: message }]];
}
response = {
body: responseBody,
headers,
statusCode,
};
if (!shouldStream || respondWith === 'binary') {
this.sendResponse(response);
}
} catch (error) {
if (this.continueOnFail()) {
const itemData = generatePairedItemData(items.length);
const returnData = this.helpers.constructExecutionMetaData(
[{ json: { error: error.message } }],
{ itemData },
);
return [returnData];
}
throw error;
}
if (nodeVersion === 1.3) {
return [items, [{ json: { response } }]];
} else if (nodeVersion >= 1.4 && this.getNodeParameter('enableResponseOutput', 0, false)) {
return [items, [{ json: { response } }]];
}
return [items];
}
}
@@ -0,0 +1,614 @@
import type { DeepMockProxy } from 'jest-mock-extended';
import { mock, mockDeep } from 'jest-mock-extended';
import { constructExecutionMetaData } from 'n8n-core';
import {
BINARY_ENCODING,
WAIT_NODE_TYPE,
type IExecuteFunctions,
type INode,
type INodeExecutionData,
type NodeTypeAndVersion,
CHAT_TRIGGER_NODE_TYPE,
} from 'n8n-workflow';
import { RespondToWebhook } from '../RespondToWebhook.node';
describe('RespondToWebhook Node', () => {
let respondToWebhook: RespondToWebhook;
let mockExecuteFunctions: DeepMockProxy<IExecuteFunctions>;
beforeEach(() => {
respondToWebhook = new RespondToWebhook();
mockExecuteFunctions = mockDeep<IExecuteFunctions>({
helpers: { constructExecutionMetaData },
});
});
describe('chatTrigger response', () => {
it('should handle chatTrigger correctly when enabled and responseBody is an object', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.4 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({
type: CHAT_TRIGGER_NODE_TYPE,
disabled: false,
parameters: { options: { responseMode: 'responseNodes' } },
}),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'responseBody') return { message: 'Hello World' };
if (paramName === 'options') return {};
});
mockExecuteFunctions.putExecutionToWait.mockResolvedValue();
const result = await respondToWebhook.execute.call(mockExecuteFunctions);
expect(result).toEqual([[{ json: {}, sendMessage: 'Hello World' }]]);
});
it('should handle chatTrigger correctly when enabled and responseBody is not an object', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({
type: CHAT_TRIGGER_NODE_TYPE,
disabled: false,
parameters: { options: { responseMode: 'responseNodes' } },
}),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'text';
if (paramName === 'responseBody') return 'Just a string';
if (paramName === 'options') return {};
});
mockExecuteFunctions.putExecutionToWait.mockResolvedValue();
const result = await respondToWebhook.execute.call(mockExecuteFunctions);
expect(result).toEqual([[{ json: {}, sendMessage: '' }]]);
});
it('should not handle chatTrigger when disabled', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: CHAT_TRIGGER_NODE_TYPE, disabled: true }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'responseBody') return { message: 'Hello World' };
if (paramName === 'options') return {};
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalled();
});
it('should return input data onMessage call', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
const result = await respondToWebhook.onMessage(mockExecuteFunctions, {
json: { message: '' },
});
expect(result).toEqual([[{ json: { input: true } }]]);
});
});
describe('execute method', () => {
it('should throw an error if no WEBHOOK_NODE_TYPES in parents', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: 'n8n-nodes-base.someNode' }),
]);
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).rejects.toThrow(
'No Webhook node found in the workflow',
);
});
it('should not throw an error if WEBHOOK_NODE_TYPES is in parents', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockReturnValue('text');
mockExecuteFunctions.getNodeParameter.mockReturnValue({});
mockExecuteFunctions.getNodeParameter.mockReturnValue('noData');
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
});
it('should correctly apply response options', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'firstIncomingItem';
if (paramName === 'options')
return {
responseHeaders: { entries: [{ name: 'X-My-Header', value: 'X-My-Header' }] },
responseCode: 201,
responseKey: 'data',
};
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: { data: { input: true } },
headers: { 'x-my-header': 'X-My-Header' },
statusCode: 201,
});
});
it('should correctly return a json response', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'options') return {};
if (paramName === 'responseBody') return { response: true };
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: { response: true },
headers: {},
statusCode: 200,
});
});
it('should correctly return a stringified json response', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'options') return {};
if (paramName === 'responseBody') return JSON.stringify({ response: true });
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: { response: true },
headers: {},
statusCode: 200,
});
});
it('should correctly return a jwt response', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getCredentials.mockResolvedValue(
mock({
keyType: 'passphrase',
privateKey: 'privateKey',
secret: 'secret',
algorithm: 'HS256',
}),
);
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'jwt';
if (paramName === 'options') return {};
if (paramName === 'payload') return 'payload';
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: {
token: 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.4GMt2k_zZryxhKgC8_HvdSZtYxyEyDa0AFIL-n60a8M',
},
headers: {},
statusCode: 200,
});
});
it('should correctly return a text response', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'text';
if (paramName === 'options') return {};
if (paramName === 'responseBody') return 'responseBody';
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: 'responseBody',
headers: {},
statusCode: 200,
});
});
it('should correctly return a redirect', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'redirect';
if (paramName === 'options') return {};
if (paramName === 'redirectURL') return 'https://n8n.io';
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
headers: { location: 'https://n8n.io' },
statusCode: 307,
});
});
it('should correctly return incoming items', async () => {
const inputItems = [{ json: { index: 0, input: true } }, { json: { index: 1, input: true } }];
mockExecuteFunctions.getInputData.mockReturnValue(inputItems);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'allIncomingItems';
if (paramName === 'options') return {};
});
mockExecuteFunctions.sendResponse.mockReturnValue();
const result = await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: inputItems.map((item) => item.json),
headers: {},
statusCode: 200,
});
expect(result).toHaveLength(1);
expect(result[0]).toHaveLength(2);
expect(result[0]).toEqual(inputItems);
});
it('should correctly return binary', async () => {
const binary = { data: 'text', mimeType: 'text/plain' };
const inputItems: INodeExecutionData[] = [{ binary: { data: binary }, json: {} }];
mockExecuteFunctions.getInputData.mockReturnValue(inputItems);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(binary);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'binary';
if (paramName === 'options') return {};
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: Buffer.from('text', BINARY_ENCODING),
headers: {
'content-length': 3,
'content-type': 'text/plain',
},
statusCode: 200,
});
});
it('should correctly handle continue on fail', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'notSupportedRespondWith';
if (paramName === 'options') return {};
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.toEqual([
[
{
json: { error: 'The Response Data option "notSupportedRespondWith" is not supported!' },
pairedItem: [{ item: 0 }],
},
],
]);
expect(mockExecuteFunctions.sendResponse).not.toHaveBeenCalled();
});
it('should have two outputs in version 1.3', async () => {
const inputItems = [{ json: { index: 0, input: true } }, { json: { index: 1, input: true } }];
mockExecuteFunctions.getInputData.mockReturnValue(inputItems);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.3 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'redirect';
if (paramName === 'redirectURL') return 'n8n.io';
if (paramName === 'options') return {};
});
mockExecuteFunctions.sendResponse.mockReturnValue();
const result = await respondToWebhook.execute.call(mockExecuteFunctions);
expect(result).toHaveLength(2);
expect(result).toEqual([
[
{
json: {
index: 0,
input: true,
},
},
{
json: {
index: 1,
input: true,
},
},
],
[
{
json: {
response: {
headers: {
location: 'n8n.io',
},
statusCode: 307,
},
},
},
],
]);
});
});
describe('streaming functionality', () => {
it('should stream JSON response when streaming is enabled', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'options') return { enableStreaming: true };
if (paramName === 'responseBody') return { response: true };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('begin', 0);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('item', 0, { response: true });
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('end', 0);
expect(mockExecuteFunctions.sendResponse).not.toHaveBeenCalled();
});
it('should stream text response when streaming is enabled', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'text';
if (paramName === 'options') return { enableStreaming: true };
if (paramName === 'responseBody') return 'test response';
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('begin', 0);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('item', 0, 'test response');
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('end', 0);
expect(mockExecuteFunctions.sendResponse).not.toHaveBeenCalled();
});
it('should stream JWT response when streaming is enabled', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getCredentials.mockResolvedValue(
mock({
keyType: 'passphrase',
privateKey: 'privateKey',
secret: 'secret',
algorithm: 'HS256',
}),
);
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'jwt';
if (paramName === 'options') return { enableStreaming: true };
if (paramName === 'payload') return { test: 'payload' };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('begin', 0);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('item', 0, {
token: expect.any(String),
});
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('end', 0);
expect(mockExecuteFunctions.sendResponse).not.toHaveBeenCalled();
});
it('should stream first incoming item when streaming is enabled', async () => {
const inputItems = [{ json: { test: 'data' } }];
mockExecuteFunctions.getInputData.mockReturnValue(inputItems);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'firstIncomingItem';
if (paramName === 'options') return { enableStreaming: true };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('begin', 0);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('item', 0, { test: 'data' });
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('end', 0);
expect(mockExecuteFunctions.sendResponse).not.toHaveBeenCalled();
});
it('should stream all incoming items when streaming is enabled', async () => {
const inputItems = [{ json: { item: 1 } }, { json: { item: 2 } }];
mockExecuteFunctions.getInputData.mockReturnValue(inputItems);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'allIncomingItems';
if (paramName === 'options') return { enableStreaming: true };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('begin', 0);
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('item', 0, { item: 1 });
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('item', 1, { item: 2 });
expect(mockExecuteFunctions.sendChunk).toHaveBeenCalledWith('end', 0);
expect(mockExecuteFunctions.sendResponse).not.toHaveBeenCalled();
});
it('should not stream when enableStreaming is false', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.sendResponse.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'options') return { enableStreaming: false };
if (paramName === 'responseBody') return { response: true };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).not.toHaveBeenCalled();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: { response: true },
headers: {},
statusCode: 200,
});
});
it('should not stream when context is not streaming', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(false);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.sendResponse.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'options') return { enableStreaming: true };
if (paramName === 'responseBody') return { response: true };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).not.toHaveBeenCalled();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: { response: true },
headers: {},
statusCode: 200,
});
});
it('should not stream binary responses', async () => {
const binary = { data: 'text', mimeType: 'text/plain' };
const inputItems: INodeExecutionData[] = [{ binary: { data: binary }, json: {} }];
mockExecuteFunctions.getInputData.mockReturnValue(inputItems);
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(binary);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.5 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.sendResponse.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'binary';
if (paramName === 'options') return { enableStreaming: true };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).not.toHaveBeenCalled();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: Buffer.from('text', BINARY_ENCODING),
headers: {
'content-length': 3,
'content-type': 'text/plain',
},
statusCode: 200,
});
});
it('should use non-streaming mode for older versions', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.4 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: WAIT_NODE_TYPE }),
]);
mockExecuteFunctions.isStreaming.mockReturnValue(true);
mockExecuteFunctions.sendChunk.mockImplementation(() => {});
mockExecuteFunctions.sendResponse.mockImplementation(() => {});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'options') return {};
if (paramName === 'responseBody') return { response: true };
});
await respondToWebhook.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.sendChunk).not.toHaveBeenCalled();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalledWith({
body: { response: true },
headers: {},
statusCode: 200,
});
});
});
});
@@ -0,0 +1,63 @@
import type { IDataObject } from 'n8n-workflow';
import { BINARY_ENCODING } from 'n8n-workflow';
import { getBinaryResponse } from '../utils/binary';
describe('getBinaryResponse', () => {
it('returns { binaryData } when binaryData.id is present', () => {
const binaryData = {
id: '123',
data: '<h1>Hello</h1>',
mimeType: 'text/html',
};
const headers: IDataObject = {};
const result = getBinaryResponse(binaryData, headers);
expect(result).toEqual({ binaryData });
expect(headers['content-type']).toBe('text/html');
});
it('returns { binaryData } when binaryData.id is present and mimeType is not text/html', () => {
const binaryData = {
id: '123',
data: 'some-binary-data',
mimeType: 'application/octet-stream',
};
const headers: IDataObject = {};
const result = getBinaryResponse(binaryData, headers);
expect(result).toEqual({ binaryData });
expect(headers['content-type']).toBe('application/octet-stream');
});
it('returns Buffer when binaryData.id is not present', () => {
const binaryData = {
data: '<h1>Hello</h1>',
mimeType: 'text/html',
};
const headers: IDataObject = {};
const result = getBinaryResponse(binaryData, headers);
expect(Buffer.isBuffer(result)).toBe(true);
expect(result.toString()).toBe(Buffer.from(binaryData.data, BINARY_ENCODING).toString());
expect(headers['content-type']).toBe('text/html');
});
it('returns Buffer when binaryData.id is not present and mimeType is not text/html', () => {
const binaryData = {
data: 'some-binary-data',
mimeType: 'application/octet-stream',
};
const headers: IDataObject = {};
const result = getBinaryResponse(binaryData, headers);
expect(Buffer.isBuffer(result)).toBe(true);
expect(result.toString()).toBe(Buffer.from(binaryData.data, BINARY_ENCODING).toString());
expect(headers['content-type']).toBe('application/octet-stream');
expect(headers['content-length']).toBe(Buffer.from(binaryData.data, BINARY_ENCODING).length);
});
});
@@ -0,0 +1,37 @@
import { configuredOutputs } from '../utils/outputs';
describe('configuredOutputs', () => {
it('returns array of objects when version >= 1.3', () => {
const result = configuredOutputs(1.3, {});
expect(result).toEqual([
{ type: 'main', displayName: 'Input Data' },
{ type: 'main', displayName: 'Response' },
]);
});
it('returns array of objects when version > 1.4 and enableResponseOutput', () => {
const result = configuredOutputs(2, { enableResponseOutput: true });
expect(result).toEqual([
{ type: 'main', displayName: 'Input Data' },
{ type: 'main', displayName: 'Response' },
]);
});
it('returns ["main"] when version < 1.3', () => {
const result = configuredOutputs(1.2, {});
expect(result).toEqual(['main']);
});
it('returns array of objects when version 1.4 and enableResponseOutput', () => {
const result = configuredOutputs(1.4, { enableResponseOutput: true });
expect(result).toEqual([
{ type: 'main', displayName: 'Input Data' },
{ type: 'main', displayName: 'Response' },
]);
});
it('returns ["main"] when version 1.4 and !enableResponseOutput', () => {
const result = configuredOutputs(1.4, { enableResponseOutput: false });
expect(result).toEqual(['main']);
});
});
@@ -0,0 +1,30 @@
import type { IBinaryData, IDataObject, IN8nHttpResponse } from 'n8n-workflow';
import { BINARY_ENCODING } from 'n8n-workflow';
import type { Readable } from 'stream';
const setContentLength = (responseBody: IN8nHttpResponse | Readable, headers: IDataObject) => {
if (Buffer.isBuffer(responseBody)) {
headers['content-length'] = responseBody.length;
} else if (typeof responseBody === 'string') {
headers['content-length'] = Buffer.byteLength(responseBody, 'utf8');
}
};
/**
* Returns a response body for a binary data and sets the content-type header.
*/
export const getBinaryResponse = (binaryData: IBinaryData, headers: IDataObject) => {
let responseBody: IN8nHttpResponse | Readable;
if (binaryData.id) {
responseBody = { binaryData };
} else {
const responseBuffer = Buffer.from(binaryData.data, BINARY_ENCODING);
responseBody = responseBuffer;
setContentLength(responseBody, headers);
}
headers['content-type'] ??= binaryData.mimeType;
return responseBody;
};
@@ -0,0 +1,20 @@
export const configuredOutputs = (
version: number,
parameters: { enableResponseOutput?: boolean },
) => {
const multipleOutputs = version === 1.3 || (version >= 1.4 && parameters.enableResponseOutput);
if (multipleOutputs) {
return [
{
type: 'main',
displayName: 'Input Data',
},
{
type: 'main',
displayName: 'Response',
},
];
}
return ['main'];
};
@@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.7459 16.6002C17.0839 19.3911 15.4846 22.1036 13.8539 24.8005C13.4306 25.4904 13.2268 26.0548 13.556 26.9329C14.4811 29.3632 13.1797 31.7151 10.7337 32.3579C8.42888 32.9694 6.18674 31.4485 5.73204 28.9869C5.32437 26.8074 7.01774 24.6594 9.43236 24.3144C9.63619 24.2831 9.84002 24.2831 10.185 24.2517C11.3609 22.2761 12.5682 20.2535 13.8539 18.1054C11.5491 15.8162 10.185 13.135 10.4829 9.8267C10.7024 7.47479 11.6118 5.45216 13.3052 3.79015C16.5351 0.607237 21.4584 0.0898187 25.2528 2.5358C28.9061 4.8877 30.5681 9.45039 29.157 13.3545C28.0751 13.0566 26.9932 12.7744 25.8173 12.4451C26.272 10.2657 25.927 8.32148 24.4689 6.64379C23.4967 5.54623 22.2581 4.9661 20.8469 4.74659C18.009 4.32324 15.2337 6.14205 14.4027 8.91729C13.462 12.0845 14.8731 14.6559 18.7459 16.6002Z" fill="#E7EBF3"/>
<path d="M23.4967 13.2919C24.6726 15.3615 25.8642 17.4626 27.0402 19.5322C32.9984 17.6821 37.4983 20.9904 39.1133 24.5183C41.0575 28.7831 39.7248 33.8475 35.899 36.4659C31.9792 39.1628 27.0088 38.7081 23.528 35.2273C24.4217 34.4903 25.2998 33.7377 26.2562 32.9381C29.69 35.1646 32.7004 35.0548 34.9269 32.4207C36.8241 30.1785 36.7928 26.8231 34.8328 24.6123C32.575 22.0723 29.5489 21.9939 25.8956 24.4399C24.3747 21.743 22.8381 19.0775 21.3643 16.365C20.8625 15.4556 20.3294 14.9225 19.2005 14.7344C17.3347 14.4051 16.1274 12.8058 16.0647 11.0183C15.9863 9.24658 17.0368 7.64729 18.6674 7.02011C20.2824 6.40862 22.1796 6.89468 23.2772 8.27446C24.1709 9.40338 24.4531 10.6577 23.9827 12.0532C23.8573 12.4608 23.6848 12.8372 23.4967 13.2919Z" fill="#E7EBF3"/>
<path d="M26.3032 30.1942H19.122C18.4322 33.0165 16.9426 35.3057 14.3869 36.7638C12.3956 37.8928 10.2475 38.2691 7.97403 37.9084C3.77196 37.2342 0.338184 33.4398 0.024597 29.1907C-0.320349 24.3615 3.00367 20.081 7.42525 19.1089C7.72316 20.2221 8.03675 21.3354 8.35033 22.4486C4.28938 24.5183 2.89392 27.121 4.02283 30.3823C5.02631 33.2517 7.86427 34.8196 10.9374 34.2238C14.089 33.5966 15.6726 30.9938 15.4844 26.7918C18.4635 26.7918 21.4583 26.7604 24.4373 26.8074C25.5976 26.8231 26.507 26.6977 27.3851 25.6785C28.8276 23.9852 31.4931 24.142 33.0453 25.7413C34.6289 27.3719 34.5662 30.006 32.8728 31.5583C31.2422 33.0635 28.6865 32.9851 27.1656 31.3545C26.852 31.0095 26.6168 30.6019 26.3032 30.1942Z" fill="#E7EBF3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path fill="#37474f" d="M35 37c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4"/><path fill="#37474f" d="M35 43c-3 0-5.9-1.4-7.8-3.7l3.1-2.5c1.1 1.4 2.9 2.3 4.7 2.3 3.3 0 6-2.7 6-6s-2.7-6-6-6c-1 0-2 .3-2.9.7l-1.7 1L23.3 16l3.5-1.9 5.3 9.4c1-.3 2-.5 3-.5 5.5 0 10 4.5 10 10S40.5 43 35 43"/><path fill="#37474f" d="M14 43C8.5 43 4 38.5 4 33c0-4.6 3.1-8.5 7.5-9.7l1 3.9C9.9 27.9 8 30.3 8 33c0 3.3 2.7 6 6 6s6-2.7 6-6v-2h15v4H23.8c-.9 4.6-5 8-9.8 8"/><path fill="#e91e63" d="M14 37c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4"/><path fill="#37474f" d="M25 19c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4"/><path fill="#e91e63" d="m15.7 34-3.4-2 5.9-9.7c-2-1.9-3.2-4.5-3.2-7.3 0-5.5 4.5-10 10-10s10 4.5 10 10c0 .9-.1 1.7-.3 2.5l-3.9-1c.1-.5.2-1 .2-1.5 0-3.3-2.7-6-6-6s-6 2.7-6 6c0 2.1 1.1 4 2.9 5.1l1.7 1z"/></svg>

After

Width:  |  Height:  |  Size: 876 B