Help me fix error "webhooks is not iterable" when doing trigger node

I am getting the error as shown in the following image when making a node trigger and I don’t know the cause of the error or the solution to this error. I hope the n8n community can support me

Hi @Hien_Le, welcome to the community and I am very sorry for the trouble.

It looks like you are trying to build a custom trigger node here. Perhaps @marcus from our node engineering team can help with this?

In the meantime, perhaps you can share the repository for your custom node for a closer look at the code throwing this error?

Code:

import type {
	IHookFunctions,
	IWebhookFunctions,
	IDataObject,
	INodeType,
	INodeTypeDescription,
	IWebhookResponseData,
} from 'n8n-workflow';
import { sapoApiRequest } from './GenericFunction';

export class SapoTrigger implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Sapo Trigger',
		name: 'sapoTrigger',
		// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
		icon: 'file:sapoLogo.png',
		group: ['trigger'],
		version: 1,
		subtitle: '={{$parameter["event"]}}',
		description: 'Handle Sapo events via webhooks',
		defaults: {
			name: 'Sapo Trigger',
		},
		triggerPanel: {
			header: '',
			executionsHelp: {
				inactive:
					'Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. <a data-key="activate">Activate</a> the workflow, then make requests to the production URL. These executions will show up in the executions list, but not in the editor.',
				active:
					'Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. Since the workflow is activated, you can make requests to the production URL. These executions will show up in the <a data-key="executions">executions list</a>, but not in the editor.',
			},
			activationHint:
				'Once you’ve finished building your workflow, run it without having to click this button by using the production webhook URL.',
		},
		inputs: [],
		outputs: ['main'],
		credentials: [
			{
				name: 'sapoTriggerApi',
				required: true,
			},
		],
		webhooks: [
			{
				name: 'default',
				httpMethod: 'POST',
				isFullPath: true,
				responseMode: 'onReceived',
				path: '={{$parameter["path"]}}',
			},
		],
		properties: [
			{
				displayName: 'Path',
				name: 'path',
				type: 'string',
				default: '',
				placeholder: 'webhook',
				required: true,
				description: 'The path to listen to',
			},
			{
				displayName: 'Event',
				name: 'event',
				type: 'options',
				noDataExpression: true,
				required: true,
				// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-options
				default: '',
				options: [
					{
						name: 'Delete Customer',
						value: 'customers/delete',
						description: 'Triggers when customer is deleted',
					},
					{
						name: 'Delete Order',
						value: 'orders/delete',
						description: 'Triggers when order is deleted',
					},
					// eslint-disable-next-line n8n-nodes-base/node-param-option-value-duplicate
					{
						name: 'Delete Product',
						value: 'products/delete',
						description: 'Triggers when product is deleted',
					},
					{
						name: 'New Cancelled Order',
						value: 'orders/cancelled',
						description: 'Triggers when order is cancelled',
					},
					{
						name: 'New Customer',
						value: 'customers/create',
						description: 'Triggers when a new customer is created',
					},
					{
						name: 'New Fulfilled Order',
						value: 'orders/fulfilled',
						description: 'Triggers when order is fulfilled',
					},
					{
						name: 'New Order',
						value: 'orders/create',
						description: 'Triggers when a new order is created',
					},
					{
						name: 'New Paid Order',
						value: 'orders/paid',
						description: 'Triggers when a paid order is created',
					},
					{
						name: 'New Partially Fulfilled Order',
						value: 'orders/partially_fulfilled',
						description: 'Triggers when order is partially fulfilled',
					},
					{
						name: 'New Product',
						value: 'products/create',
						description: 'Triggers when a new product is created',
					},
					{
						name: 'Update Customer',
						value: 'customers/update',
						description: 'Triggers when customer is updated',
					},
					{
						name: 'Update Order',
						value: 'orders/updated',
						description: 'Triggers when order is updated',
					},
					{
						name: 'Update Product',
						value: 'products/update',
						description: 'Triggers when product is updated',
					},
				],
			},
		],
	};

	webhookMethods = {
		default: {
			async checkExists(this: IHookFunctions): Promise<boolean> {
				const webhookData = this.getWorkflowStaticData('node');
				const webhookUrl = this.getNodeWebhookUrl('default');
				const event = this.getNodeParameter('event') as string;
				const { hooks: webhooks } = await sapoApiRequest.call(this, 'GET', '/webhooks.json');
				for (const webhook of webhooks) {
					if (webhook.address === webhookUrl && webhook.topic === event) {
						webhookData.webhookId = webhook.id;
						return true;
					}
				}
				return false;
			},
			async create(this: IHookFunctions): Promise<boolean> {
				const webhookUrl = this.getNodeWebhookUrl('default');
				const webhookData = this.getWorkflowStaticData('node');
				const event = this.getNodeParameter('event') as string;
				const body: IDataObject = {
					topic: event,
					target_url: webhookUrl,
					format: 'json',
				};
				const webhook = await sapoApiRequest.call(this, 'POST', '/webhooks.json', body);
				webhookData.webhookId = webhook.id;
				return true;
			},
			async delete(this: IHookFunctions): Promise<boolean> {
				const webhookData = this.getWorkflowStaticData('node');
				try {
					await sapoApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.webhookId}.json`);
				} catch (error) {
					return false;
				}
				delete webhookData.webhookId;
				return true;
			},
		},
	};

	async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
		const req = this.getRequestObject();
		return {
			workflowData: [this.helpers.returnJsonArray(req.body as IDataObject[])],
		};
	}
}

Hi @Hien_Le,
I just tried to load your SapoTrigger node and I was able to make it run without the error webhook is not iterable.

  • Which version of n8n are you using?
  • Are you using our n8n-nodes-starter project?
  • Is your node code publicly hosted on github or similar?

Is there a reason why you are a custom path for your webhook?

webhooks: [
	{
		name: 'default',
		httpMethod: 'POST',
		isFullPath: true,
		responseMode: 'onReceived',
		path: '={{$parameter["path"]}}',
	},
],

Normally the following should suffice for webhook based trigger nodes.

webhooks: [
	{
		name: 'default',
		httpMethod: 'POST',
		responseMode: 'onReceived',
		path: 'webhook',
	},
],

I changed the webhook to not allow the path to be edited, but the error still remains the same. I don’t think I’m getting an error due to the custom path. Can you further assist me on how to fix this error?

Hey @Hien_Le,
I can’t test out the real webhook functions checkExists, create and delete since I am missing the sapiApiRequest and credentials, but I think the error could come from the checkExists method not returning the expected array (iterable) that you are expecting.

const { hooks: webhooks } = await sapoApiRequest.call(this, 'GET', '/webhooks.json');
for (const webhook of webhooks) {
	...
}

I am guessing the error happens when you click Listen for Event and the node wants to check if the webhook already exists and if not try to create it.

Below is my sapiApiRequest function, you can take a look to understand clearly what the problem is:

// TEST SAPO TRIGGER
export async function sapoApiRequest(
	this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions,
	method: string,
	resource: string,
	body: any = {},
	query: IDataObject = {},
	uri?: string,
	_option: IDataObject = {},
): Promise<any> {
	const credentials = await this.getCredentials('sapoTriggerApi');

	const accessToken = `${credentials.accessToken}`;

	const storeName = `${credentials.storeName}`;
	const endpoint = `https://${storeName}.mysapo.net/admin`;

	const requestBody = {
		webhook: {
			topic: body.topic,
			address: body.target_url,
			format: body.format
		}
	}

	const options: OptionsWithUri = {
		headers: {
			'Content-Type': 'application/json',
			'X-Sapo-Access-Token': accessToken,
		},
		method,
		body: requestBody,
		qs: query,
		uri: uri || `${endpoint}${resource}`,
		json: true,
	};
	if (!Object.keys(body as IDataObject).length) {
		delete options.body;
	}
	if (!Object.keys(query).length) {
		delete options.qs;
	}

	try {
		return await this.helpers.request(options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error as JsonObject);
	}
}

And here is my Trigger code:

import type {
	IHookFunctions,
	IWebhookFunctions,
	IDataObject,
	INodeType,
	INodeTypeDescription,
	IWebhookResponseData,
} from 'n8n-workflow';
import { sapoApiRequest } from './GenericFunction';

export class SapoTrigger implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Sapo Trigger',
		name: 'sapoTrigger',
		// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
		icon: 'file:sapoLogo.png',
		group: ['trigger'],
		version: 1,
		subtitle: '={{$parameter["event"]}}',
		description: 'Handle Sapo events via webhooks',
		defaults: {
			name: 'Sapo Trigger',
		},
		triggerPanel: {
			header: '',
			executionsHelp: {
				inactive:
					'Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. <a data-key="activate">Activate</a> the workflow, then make requests to the production URL. These executions will show up in the executions list, but not in the editor.',
				active:
					'Webhooks have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. Since the workflow is activated, you can make requests to the production URL. These executions will show up in the <a data-key="executions">executions list</a>, but not in the editor.',
			},
			activationHint:
				'Once you’ve finished building your workflow, run it without having to click this button by using the production webhook URL.',
		},
		inputs: [],
		outputs: ['main'],
		credentials: [
			{
				name: 'sapoTriggerApi',
				required: true,
			},
		],
		webhooks: [
			{
				name: 'default',
				httpMethod: 'POST',
				responseMode: 'onReceived',
				path: 'webhook',
			},
		],
		properties: [
			{
				displayName: 'Event',
				name: 'event',
				type: 'options',
				noDataExpression: true,
				required: true,
				// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-options
				default: '',
				options: [
					{
						name: 'Delete Customer',
						value: 'customers/delete',
						description: 'Triggers when customer is deleted',
					},
					{
						name: 'Delete Order',
						value: 'orders/delete',
						description: 'Triggers when order is deleted',
					},
					// eslint-disable-next-line n8n-nodes-base/node-param-option-value-duplicate
					{
						name: 'Delete Product',
						value: 'products/delete',
						description: 'Triggers when product is deleted',
					},
					{
						name: 'New Cancelled Order',
						value: 'orders/cancelled',
						description: 'Triggers when order is cancelled',
					},
					{
						name: 'New Customer',
						value: 'customers/create',
						description: 'Triggers when a new customer is created',
					},
					{
						name: 'New Fulfilled Order',
						value: 'orders/fulfilled',
						description: 'Triggers when order is fulfilled',
					},
					{
						name: 'New Order',
						value: 'orders/create',
						description: 'Triggers when a new order is created',
					},
					{
						name: 'New Paid Order',
						value: 'orders/paid',
						description: 'Triggers when a paid order is created',
					},
					{
						name: 'New Partially Fulfilled Order',
						value: 'orders/partially_fulfilled',
						description: 'Triggers when order is partially fulfilled',
					},
					{
						name: 'New Product',
						value: 'products/create',
						description: 'Triggers when a new product is created',
					},
					{
						name: 'Update Customer',
						value: 'customers/update',
						description: 'Triggers when customer is updated',
					},
					{
						name: 'Update Order',
						value: 'orders/updated',
						description: 'Triggers when order is updated',
					},
					{
						name: 'Update Product',
						value: 'products/update',
						description: 'Triggers when product is updated',
					},
				],
			},
		],
	};

	webhookMethods = {
		default: {
			async checkExists(this: IHookFunctions): Promise<boolean> {
				const webhookData = this.getWorkflowStaticData('node');
				const event = this.getNodeParameter('event') as string;
				const webhooks = await sapoApiRequest.call(this, 'GET', '/webhooks.json');
					for (const webhook of webhooks) {
						if (webhook.topic === event) {
							webhookData.webhookId = webhook.id;
							return true;
						}
					}
				return false;
			},
			async create(this: IHookFunctions): Promise<boolean> {
				const webhookUrl = this.getNodeWebhookUrl('default');
				const webhookData = this.getWorkflowStaticData('node');
				const event = this.getNodeParameter('event') as string;
				const body: IDataObject = {
					topic: event,
					target_url: webhookUrl,
					format: 'json',
				};
				const webhook = await sapoApiRequest.call(this, 'POST', '/webhooks.json', body);
				webhookData.webhookId = webhook.id;
				return true;
			},
			async delete(this: IHookFunctions): Promise<boolean> {
				const webhookData = this.getWorkflowStaticData('node');
				try {
					await sapoApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.webhookId}.json`);
				} catch (error) {
					return false;
				}
				delete webhookData.webhookId;
				return true;
			},
		},
	};

	async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
		const req = this.getRequestObject();
		return {
			workflowData: [this.helpers.returnJsonArray(req.body as IDataObject[])],
		};
	}
}

Hey @Hien_Le,

That error is saying that the webhooks variable you are looping over in the check exists is not an array.

Have you checked the output from the api to make sure it is correct and popped a console.log() in there to see what the response is to make sure there are not issues there?

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