node-firebird 2.16.1 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,6 +181,7 @@ same way `PGUSER`/`PGPASSWORD` work with pg.
181
181
  var options = {};
182
182
 
183
183
  options.host = '127.0.0.1';
184
+ options.eventHost = undefined; // optional; override the server-advertised host for the auxiliary event connection
184
185
  options.port = 3050;
185
186
  options.database = 'database.fdb';
186
187
  options.user = 'SYSDBA';
@@ -1434,6 +1435,54 @@ Firebird.attach(options, function (err, db) {
1434
1435
  });
1435
1436
  ```
1436
1437
 
1438
+ #### The auxiliary connection, and when `attachEvent` fails
1439
+
1440
+ Events do **not** travel over the connection you attached with. `db.attachEvent()` sends
1441
+ `op_connect_request`; the server opens a fresh listening socket, replies with its address and
1442
+ port, and the driver dials that address as a **second, independent TCP connection**. Only once
1443
+ that aux socket is up is the `FbEventManager` created and handed to your callback.
1444
+
1445
+ The aux port is chosen by the server and is *not* the port you connected to, so it has to be
1446
+ reachable from the client in its own right. Two deployments commonly get this wrong:
1447
+
1448
+ - **Containers.** A Firebird container that publishes only `3050` answers `op_connect_request`
1449
+ with an aux port nothing outside the container can reach. Pin it with `RemoteAuxPort` in
1450
+ `firebird.conf` and publish that port too, or run the client on the same network.
1451
+ - **Firewalls.** A rule that DROPs the aux port (rather than REJECTing it) leaves the dial
1452
+ waiting out the operating system's connect timeout — around two minutes on Linux — before
1453
+ the failure is reported.
1454
+
1455
+ If the server reports `0.0.0.0` or `::` as the aux address — usual when it listens on all
1456
+ interfaces — the driver dials the host from your connection options instead, so that host must
1457
+ be the one reaching the aux port.
1458
+
1459
+ Behind NAT, a tunnel, container networking, or a load balancer, the advertised
1460
+ address may not be reachable from the client. Set `options.eventHost` to override
1461
+ only the host used for the auxiliary event connection. The auxiliary port is
1462
+ still selected by Firebird; use `RemoteAuxPort` when it also needs to be fixed.
1463
+
1464
+ Since **2.16.2** a failed dial is reported to the attachment callback as an `Error`, exactly
1465
+ once, carrying Node's socket `code`. Earlier versions recorded it internally and never called
1466
+ back, so `attachEvent()` and `attachEventAsync()` hung indefinitely:
1467
+
1468
+ ```js
1469
+ db.attachEvent(function (err, evtmgr) {
1470
+ if (err) {
1471
+ // ECONNREFUSED → aux port not reachable (unpublished container port, nothing listening)
1472
+ // ETIMEDOUT → aux port filtered by a firewall
1473
+ // EHOSTUNREACH → wrong host: the address the server reported, or your connection host
1474
+ // when the server reported 0.0.0.0/::
1475
+ console.error('event attachment failed:', err.code || err.message);
1476
+ return;
1477
+ }
1478
+ // ...
1479
+ });
1480
+ ```
1481
+
1482
+ Errors on the aux socket *after* it connects are not delivered to this callback. Poll
1483
+ `evtmgr.getState().isEventConnectionOpen` if you need to detect an aux connection that dies
1484
+ mid-subscription.
1485
+
1437
1486
  ### Escaping Query values
1438
1487
 
1439
1488
  ```js
@@ -2429,6 +2478,21 @@ If the failure is intermittent — the same code with the same credentials succe
2429
2478
 
2430
2479
  If the failure is consistent, the credentials really don't match an account for the authentication plugin in use: check `AuthServer`/`UserManager` in `firebird.conf` and remember that SRP and Legacy user managers keep separate password stores — a user created under one plugin does not automatically exist for the other.
2431
2480
 
2481
+ #### `db.attachEvent()` never calls back, or hangs, in Docker or behind a firewall
2482
+
2483
+ Upgrade to **2.16.2 or later**: before that release the driver recorded a failure to open the
2484
+ auxiliary event connection internally and never invoked the attachment callback, so
2485
+ `attachEvent()` silently stalled and `attachEventAsync()` returned a promise that never settled.
2486
+ From 2.16.2 the failure reaches your callback as an `Error` carrying Node's socket `code`.
2487
+
2488
+ The failure itself is a reachability problem, not a driver bug, and upgrading only makes it
2489
+ visible. Firebird events use a **second TCP connection** to a port the *server* chooses, which
2490
+ is not the port you connected to, so publishing `3050` alone is not enough: pin `RemoteAuxPort`
2491
+ in `firebird.conf` and publish that port too. See
2492
+ [§ The auxiliary connection](#the-auxiliary-connection-and-when-attachevent-fails) for the full
2493
+ picture, including the `0.0.0.0`/`::` case and why a DROP firewall rule delays the error by
2494
+ roughly two minutes.
2495
+
2432
2496
  ## Contributing
2433
2497
 
2434
2498
  Contributions are welcome — code, documentation, and bug reports alike.
package/lib/types.d.ts CHANGED
@@ -352,6 +352,8 @@ export interface Statement {
352
352
  export type SupportedCharacterSet = 'NONE' | 'CP943C' | 'DOS737' | 'DOS775' | 'DOS858' | 'DOS862' | 'DOS864' | 'DOS866' | 'DOS869' | 'GB18030' | 'GBK' | 'ISO8859_1' | 'ISO8859_2' | 'ISO8859_3' | 'ISO8859_4' | 'ISO8859_5' | 'ISO8859_6' | 'ISO8859_7' | 'ISO8859_8' | 'ISO8859_9' | 'ISO8859_13' | 'KOI8R' | 'KOI8U' | 'TIS620' | 'UTF8' | 'WIN1251' | 'WIN1252' | 'WIN1253' | 'WIN1254' | 'WIN1255' | 'WIN1256' | 'WIN1257' | 'WIN1258' | 'WIN_1258';
353
353
  export interface Options {
354
354
  host?: string;
355
+ /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
356
+ eventHost?: string;
355
357
  port?: number;
356
358
  database?: string;
357
359
  user?: string;
package/lib/utils.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import type { FbStatusItem } from './callback';
2
+ /** Resolve the address used for Firebird's auxiliary event connection. */
3
+ export declare function resolveEventHost(options: {
4
+ host?: string;
5
+ eventHost?: string;
6
+ }, advertisedHost: string): string;
2
7
  /**
3
8
  * Parse date from string
4
9
  */
package/lib/utils.js CHANGED
@@ -4,10 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.escape = exports.lookupMessages = exports.batchResultToError = exports.parseDate = void 0;
7
+ exports.resolveEventHost = resolveEventHost;
7
8
  exports.noop = noop;
8
9
  const firebird_msg_json_1 = __importDefault(require("./firebird.msg.json"));
9
10
  const const_1 = __importDefault(require("./wire/const"));
10
11
  const MessagesError = firebird_msg_json_1.default;
12
+ /** Resolve the address used for Firebird's auxiliary event connection. */
13
+ function resolveEventHost(options, advertisedHost) {
14
+ if (options.eventHost)
15
+ return options.eventHost;
16
+ if (advertisedHost === '0.0.0.0' || advertisedHost === '::')
17
+ return options.host || const_1.default.DEFAULT_HOST;
18
+ return advertisedHost;
19
+ }
11
20
  /**
12
21
  * Parse date from string
13
22
  */
@@ -404,9 +404,7 @@ class Database extends events_1.default.EventEmitter {
404
404
  if (process.env.FIREBIRD_DEBUG) {
405
405
  console.log('[fb-debug] Database.attachEvent: auxConnection ok, connecting to aux port %s:%d', socket_info.host, socket_info.port);
406
406
  }
407
- const host = (socket_info.host === '0.0.0.0' || socket_info.host === '::')
408
- ? self.connection.options.host
409
- : socket_info.host;
407
+ const host = (0, utils_1.resolveEventHost)(self.connection.options, socket_info.host);
410
408
  const eventConnection = new eventConnection_1.default(host, socket_info.port, function (err) {
411
409
  if (err) {
412
410
  if (process.env.FIREBIRD_DEBUG) {
@@ -9,8 +9,9 @@ declare class EventConnection {
9
9
  _xdr?: XdrReader;
10
10
  error: any;
11
11
  eventcallback: ((err: any, ret?: any) => void) | null;
12
- constructor(host: string, port: number, callback: (() => void) | undefined, db: any);
13
- _bind_events(host: string, port: number, callback?: () => void): void;
12
+ _connectSettled: boolean;
13
+ constructor(host: string, port: number, callback: ((err?: Error) => void) | undefined, db: any);
14
+ _bind_events(host: string, port: number, callback?: (err?: Error) => void): void;
14
15
  throwClosed(callback?: (err: any) => void): this;
15
16
  }
16
17
  export = EventConnection;
@@ -13,6 +13,7 @@ class EventConnection {
13
13
  this.emgr = null;
14
14
  this._isClosed = false;
15
15
  this._isOpened = false;
16
+ this._connectSettled = false;
16
17
  this._socket = net_1.default.createConnection(port, host);
17
18
  this._bind_events(host, port, callback);
18
19
  this.error = null;
@@ -20,17 +21,31 @@ class EventConnection {
20
21
  }
21
22
  _bind_events(host, port, callback) {
22
23
  var self = this;
24
+ function finishConnect(err) {
25
+ if (self._connectSettled)
26
+ return;
27
+ self._connectSettled = true;
28
+ if (callback)
29
+ callback(err);
30
+ }
23
31
  self._socket.on('close', function () {
24
32
  self._isClosed = true;
33
+ if (!self._isOpened) {
34
+ finishConnect(self.error || new Error(`Event connection to ${host}:${port} closed before connecting.`));
35
+ }
25
36
  });
26
37
  self._socket.on('error', function (e) {
27
38
  self.error = e;
39
+ if (!self._isOpened) {
40
+ if (!self._socket.destroyed)
41
+ self._socket.destroy();
42
+ finishConnect(e);
43
+ }
28
44
  });
29
45
  self._socket.on('connect', function () {
30
46
  self._isClosed = false;
31
47
  self._isOpened = true;
32
- if (callback)
33
- callback();
48
+ finishConnect();
34
49
  });
35
50
  self._socket.on('data', function (data) {
36
51
  var xdr, buf;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.16.1",
3
+ "version": "2.17.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
package/src/types.ts CHANGED
@@ -413,6 +413,8 @@ export type SupportedCharacterSet = |
413
413
 
414
414
  export interface Options {
415
415
  host?: string;
416
+ /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
417
+ eventHost?: string;
416
418
  port?: number;
417
419
  database?: string;
418
420
  user?: string;
package/src/utils.ts CHANGED
@@ -4,6 +4,13 @@ import type { FbStatusItem } from './callback';
4
4
 
5
5
  const MessagesError = messagesJson as Record<string, string>;
6
6
 
7
+ /** Resolve the address used for Firebird's auxiliary event connection. */
8
+ export function resolveEventHost(options: { host?: string; eventHost?: string }, advertisedHost: string): string {
9
+ if (options.eventHost) return options.eventHost;
10
+ if (advertisedHost === '0.0.0.0' || advertisedHost === '::') return options.host || Const.DEFAULT_HOST;
11
+ return advertisedHost;
12
+ }
13
+
7
14
  /**
8
15
  * Parse date from string
9
16
  */
@@ -1,6 +1,6 @@
1
1
  import Events from 'events';
2
2
  import { doError, fromCallback, type Callback, type SimpleCallback } from '../callback';
3
- import { batchResultToError, escape } from '../utils';
3
+ import { batchResultToError, escape, resolveEventHost } from '../utils';
4
4
  import Const from './const';
5
5
  import { makeSqlTag, type SqlTag } from '../sql-template';
6
6
  import { computeColumnKeys, nestCell, resolveKeyTransform, resolveNestTables } from './xsqlvar';
@@ -481,9 +481,7 @@ class Database extends Events.EventEmitter {
481
481
  console.log('[fb-debug] Database.attachEvent: auxConnection ok, connecting to aux port %s:%d', socket_info.host, socket_info.port);
482
482
  }
483
483
 
484
- const host = (socket_info.host === '0.0.0.0' || socket_info.host === '::')
485
- ? self.connection.options.host
486
- : socket_info.host;
484
+ const host = resolveEventHost(self.connection.options, socket_info.host);
487
485
 
488
486
  const eventConnection = new EventConnection(
489
487
  host, socket_info.port, function(err?: any) {
@@ -13,37 +13,49 @@ class EventConnection {
13
13
  _xdr?: XdrReader;
14
14
  error: any;
15
15
  eventcallback: ((err: any, ret?: any) => void) | null;
16
+ _connectSettled: boolean;
16
17
 
17
- constructor(host: string, port: number, callback: (() => void) | undefined, db: any) {
18
+ constructor(host: string, port: number, callback: ((err?: Error) => void) | undefined, db: any) {
18
19
  var self = this;
19
20
  this.db = db;
20
21
  this.emgr = null;
21
22
  this._isClosed = false;
22
23
  this._isOpened = false;
24
+ this._connectSettled = false;
23
25
  this._socket = net.createConnection(port, host);
24
26
  this._bind_events(host, port, callback);
25
27
  this.error = null;
26
28
  this.eventcallback = null;
27
29
  }
28
30
 
29
- _bind_events(host: string, port: number, callback?: () => void): void {
31
+ _bind_events(host: string, port: number, callback?: (err?: Error) => void): void {
30
32
  var self = this;
31
33
 
32
- self._socket.on('close', function () {
34
+ function finishConnect(err?: Error) {
35
+ if (self._connectSettled) return;
36
+ self._connectSettled = true;
37
+ if (callback) callback(err);
38
+ }
33
39
 
40
+ self._socket.on('close', function () {
34
41
  self._isClosed = true;
42
+ if (!self._isOpened) {
43
+ finishConnect(self.error || new Error(`Event connection to ${host}:${port} closed before connecting.`));
44
+ }
35
45
  })
36
46
 
37
47
  self._socket.on('error', function (e) {
38
-
39
48
  self.error = e;
49
+ if (!self._isOpened) {
50
+ if (!self._socket.destroyed) self._socket.destroy();
51
+ finishConnect(e);
52
+ }
40
53
  })
41
54
 
42
55
  self._socket.on('connect', function () {
43
56
  self._isClosed = false;
44
57
  self._isOpened = true;
45
- if (callback)
46
- callback();
58
+ finishConnect();
47
59
  });
48
60
 
49
61
  self._socket.on('data', function (data: Buffer) {