fix(MQTT Node): Close connection if connection attempt fails (#10873)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run

This commit is contained in:
Tomi Turtiainen 2024-09-18 20:03:18 +03:00 committed by GitHub
parent 0a317b7072
commit ee7147c6b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 46 additions and 10 deletions

View file

@ -1,5 +1,6 @@
import { connect, type IClientOptions, type MqttClient } from 'mqtt'; import { connect, type IClientOptions, type MqttClient } from 'mqtt';
import { ApplicationError, randomString } from 'n8n-workflow'; import { ApplicationError, randomString } from 'n8n-workflow';
import { formatPrivateKey } from '@utils/utilities'; import { formatPrivateKey } from '@utils/utilities';
interface BaseMqttCredential { interface BaseMqttCredential {
@ -62,6 +63,10 @@ export const createClient = async (credentials: MqttCredential): Promise<MqttCli
const onError = (error: Error) => { const onError = (error: Error) => {
client.removeListener('connect', onConnect); client.removeListener('connect', onConnect);
client.removeListener('error', onError); client.removeListener('error', onError);
// mqtt client has an automatic reconnect mechanism that will
// keep trying to reconnect until it succeeds unless we
// explicitly close the client
client.end();
reject(new ApplicationError(error.message)); reject(new ApplicationError(error.message));
}; };

View file

@ -8,6 +8,7 @@ import {
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
NodeConnectionType, NodeConnectionType,
ensureError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { createClient, type MqttCredential } from './GenericFunctions'; import { createClient, type MqttCredential } from './GenericFunctions';
@ -116,10 +117,12 @@ export class Mqtt implements INodeType {
try { try {
const client = await createClient(credentials); const client = await createClient(credentials);
client.end(); client.end();
} catch (error) { } catch (e) {
const error = ensureError(e);
return { return {
status: 'Error', status: 'Error',
message: (error as Error).message, message: error.message,
}; };
} }
return { return {

View file

@ -1,19 +1,20 @@
import { MqttClient } from 'mqtt';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { MqttClient } from 'mqtt';
import { ApplicationError } from 'n8n-workflow';
import { createClient, type MqttCredential } from '../GenericFunctions'; import { createClient, type MqttCredential } from '../GenericFunctions';
describe('createClient', () => { describe('createClient', () => {
const mockConnect = jest.spyOn(MqttClient.prototype, 'connect').mockImplementation(function (
this: MqttClient,
) {
setImmediate(() => this.emit('connect', mock()));
return this;
});
beforeEach(() => jest.clearAllMocks()); beforeEach(() => jest.clearAllMocks());
it('should create a client with minimal credentials', async () => { it('should create a client with minimal credentials', async () => {
const mockConnect = jest.spyOn(MqttClient.prototype, 'connect').mockImplementation(function (
this: MqttClient,
) {
setImmediate(() => this.emit('connect', mock()));
return this;
});
const credentials = mock<MqttCredential>({ const credentials = mock<MqttCredential>({
protocol: 'mqtt', protocol: 'mqtt',
host: 'localhost', host: 'localhost',
@ -35,4 +36,31 @@ describe('createClient', () => {
clientId: 'testClient', clientId: 'testClient',
}); });
}); });
it('should reject with ApplicationError on connection error and close connection', async () => {
const mockConnect = jest.spyOn(MqttClient.prototype, 'connect').mockImplementation(function (
this: MqttClient,
) {
setImmediate(() => this.emit('error', new Error('Connection failed')));
return this;
});
const mockEnd = jest.spyOn(MqttClient.prototype, 'end').mockImplementation();
const credentials: MqttCredential = {
protocol: 'mqtt',
host: 'localhost',
port: 1883,
clean: true,
clientId: 'testClientId',
username: 'testUser',
password: 'testPass',
ssl: false,
};
const clientPromise = createClient(credentials);
await expect(clientPromise).rejects.toThrow(ApplicationError);
expect(mockConnect).toBeCalledTimes(1);
expect(mockEnd).toBeCalledTimes(1);
});
}); });