The node was configured with a webhook, but the opening of the link concatenated by baseurl and webhookId failed


Hello @ppz,
I think that yur custom node is trying to build the webhook URL using:

const baseUrl = this.additionalData.instanceBaseUrl;
const webhookUrl = `${baseUrl}webhook/${webhookId}`;

If instanceBaseUrl (WEBHOOK_URL) isn’t set, baseUrl is empty, so the generated URL is invalid and fails.

Try to set WEBHOOK_URL If self-hosted or in Docker, add:

environment:  
- WEBHOOK_URL=https://your-domain.com/

This ensures instance base Url is populated correctly.

Let me know if my directions are clear because your question is a little vague and I could say inaccuracies.

Thank you very much for your help. I extract the source code from the official website and run and modify it locally. I created a node similar to a form node and replaced the function of generating form nodes with a custom editor. This editor returns a piece of html code containing the form. The problem I’m encountering now is:

The link generated by the webhook trigger node serves as the entry point. Through this link, a link redirected to this custom node is implemented, but when the link is opened, it shows as unregistered
no
Judging from the content I printed, instanceBaseUrl has a value,And Webhooks also have value

Perhaps it can’t be spliced like this? Or is my overall direction of modification wrong?

Your URL logic is correct, but the webhook only works if it’s registered with n8n, which requires proper webhookMethods in your custom node and an activated workflow or a test event.

  1. Implement webhookMethods in your custom node:
  • checkExists() should confirm registration.
  • create() must call this.getNodeWebhookUrl('default') to register the webhook.
  • delete() cleans up on deactivation.
  1. Avoid building URL manually, always use this.getNodeWebhookUrl('default')

Ok. Referring to your solution, I added some new content, but I’m not sure if it’s correct. After opening it through the webhook link, the execute method was directly executed. I don’t know how to call the methods in webhoolMethods. Looking forward to your reply. Thank you


When I use this.getNodeWebhookUrl(‘default’), an error will occur. this.getNodeWebhookUrl is not a function. I hope to get your reply. Thank you

You’re calling this.getNodeWebhookUrl('default') inside the execute() method. That method is only available in the webhookMethods context (like inside create, checkExists, delete), not in execute().

ToTry to move any usage of getNodeWebhookUrl to the create() method. The execute() function should only handle the incoming webhook request.

However, the “webhookMethods” method is not set in my node. I have posted my entire code here and hope you can help me point out the areas that need to be modified

import type {
	IExecuteFunctions, IHookFunctions,
	INodeExecutionData,
	INodeProperties,
	INodeTypeDescription,
	IWebhookFunctions,
	IWebhookResponseData,
	NodeTypeAndVersion,
} from 'n8n-workflow';
import {
	INodeType,
	updateDisplayOptions,
	NodeConnectionTypes,
} from 'n8n-workflow';

import { configureWaitTillDate } from '../../utils/sendAndWait/configureWaitTillDate.util';
import { limitWaitTimeProperties } from '../../utils/sendAndWait/descriptions';
import {placeholder} from "./placeholder";

const waitTimeProperties: INodeProperties[] = [
	{
		displayName: 'Limit Wait Time',
		name: 'limitWaitTime',
		type: 'boolean',
		default: false,
		description:
			'Whether to limit the time this node should wait for a user response before execution resumes',
	},
	...updateDisplayOptions(
		{
			show: {
				limitWaitTime: [true],
			},
		},
		limitWaitTimeProperties,
	),
];

const pageProperties = updateDisplayOptions(
	{
		show: {
			operation: ['page'],
		},
	},
	[
		{
			displayName: 'HTML Template',
			name: 'html',
			type: 'string',
			typeOptions: {
				editor: 'dynamicFormEditor',
			},
			default: placeholder,
			noDataExpression: true,
			description: 'HTML template to render using the visual form editor',
		},
		...waitTimeProperties,
	],
);


export class DragForm implements INodeType {
	nodeInputData: INodeExecutionData[] = [];

	description: INodeTypeDescription = {
		displayName: 'DragForm',
		name: 'DragForm',
		icon: { light: 'file:dragForm.svg', dark: 'file:dragForm.svg' },
		group: ['input'],
		version: 1,
		description: 'Generate webforms in xsjx and pass their responses to the workflow',
		defaults: {
			name: 'DragForm',
		},
		inputs: [NodeConnectionTypes.Main],
		outputs: [NodeConnectionTypes.Main],
		webhooks: [
			{
				name: 'default',
				httpMethod: 'GET',
				responseMode: 'onReceived',
				path: '',
				restartWebhook: true,
				isFullPath: true,
				nodeType: 'dragForm',
			},
			{
				name: 'default',
				httpMethod: 'POST',
				responseMode: 'responseNode',
				path: '',
				restartWebhook: true,
				isFullPath: true,
				nodeType: 'dragForm',
			},
		],
		webhookMethods: {
			default: {
				checkExists:this.checkExists,
				create:this.create,
				delete:this.delete,
			},
		},
		properties: [
			{
				displayName: 'Page Type',
				name: 'operation',
				type: 'options',
				default: 'page',
				noDataExpression: true,
				options: [
					{
						name: 'Next Form Page',
						value: 'page',
					},
				],
			},
			...pageProperties,
		],
	};

	async webhook(context: IWebhookFunctions): Promise<IWebhookResponseData> {
		const request = context.getRequestObject();
		const response = context.getResponseObject();

		if (request.method === 'GET') {
			const inputItems =  context.evaluateExpression('{{ $input.all() }}') as Array<{
				json: { formUrl: string; html: string };
			}>;
			const html = inputItems[0]?.json?.html || '<h1>No HTML found</h1>';
			response.writeHead(200, { 'Content-Type': 'text/html' });
			response.end(html);
			return {
				webhookResponse: { status: 200 },
			};
		}
		const operation = context.getNodeParameter('operation', '') as string;

		const parentNodes = context.getParentNodes(context.getNode().name);
		const trigger = parentNodes.find(
			(node) => node.type === 'n8n-nodes-base.webhook',
		) as NodeTypeAndVersion;

		const mode = context.evaluateExpression(`{{ $('${trigger?.name}').first().json.formMode }}`) as
			| 'test'
			| 'production';

		const defineForm = context.getNodeParameter('html', false) as string;

		const method = context.getRequestObject().method;

		if (method === 'GET') {
			console.log('GET Function');
		}

		return {
			webhookResponse: { status: 200 },
			workflowData: [[returnItem]],
		};
	}

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {

		const operation = this.getNodeParameter('operation', 0);
		const html = this.getNodeParameter('html', 0);
		const items = this.getInputData();

		const baseUrl = this.additionalData.instanceBaseUrl;

		const webhookId = this.getNode().webhookId;

		const webhookUrl = `${baseUrl}webhook/${webhookId}`;

		this.sendResponse({
			headers: {
				location: webhookUrl,
			},
			statusCode: 307,
		});

		const parentNodes = this.getParentNodes(this.getNode().name);

		const childNodes = this.getChildNodes(this.getNode().name);

		const waitTill = configureWaitTillDate(this, 'root');
		await this.putExecutionToWait(waitTill);
		return [[]];
	}

	async checkExists(this: IHookFunctions): Promise<boolean> {
		const webhookUrl = this.getNodeWebhookUrl('default');
		const exists = await this.getWebhookFromDatabase(webhookUrl, 'GET');
		return !!exists;
	}

	async create(this: IHookFunctions): Promise<boolean> {
		const baseUrl = this.additionalData.instanceBaseUrl;
		const webhookId = this.getNode().webhookId;
		const webhookUrl = `${baseUrl}webhook/${webhookId}`;
		await this.addWebhookToDatabase(webhookUrl, 'GET', 'dragForm', true);
		await this.addWebhookToDatabase(webhookUrl, 'POST', 'dragForm', true);


		return true;
	}

	async delete(this: IHookFunctions): Promise<boolean> {
		const webhookUrl = this.getNodeWebhookUrl('default');
		await this.removeWebhookFromDatabase(webhookUrl, 'GET');
		return true;
	}
}

1 Like

The WebflowTrigger example shows how to use getNodeWebhookUrl inside create(), not in your execute() method.

Without these webhookMethods, n8n never actually registers your webhook, so even valid URLs will return unregistered.

Try with this please:

webhookMethods = {
  default: {
    async checkExists(this: IHookFunctions): Promise<boolean> {
      // logic to verify registration state
    },
    async create(this: IHookFunctions): Promise<boolean> {
      const url = this.getNodeWebhookUrl('default');
      // logic to register the webhook with your service and store the webhookId in static data
      return true;
    },
    async delete(this: IHookFunctions): Promise<boolean> {
      // unregister the webhook using stored webhookId
      return true;
    },
  },
};
1 Like

Thank you very much. I referred to the WebflowTrigger example above and successfully implemented the invocation of these methods. However, I am now encountering another problem. I have successfully registered the webhookUrl. By const allWebhooks = await this. WebhookRepository. The find () query to all registered webhookUrl, my custom node webhook methods included, But there was a problem when opening. Error in handling webhook request GET /webhook/524c3727-8b97-416f-988a-3c6d266a5bb4: Cannot read properties of undefined (reading ‘node’). Why is that

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.