From 1e0b116193b4837035f10e71fd426a7478fb80dd Mon Sep 17 00:00:00 2001 From: Osama Akhter Date: Thu, 5 Nov 2026 15:38:50 +0100 Subject: [PATCH] fix(pg): keep the connection error as the cause of the "not queryable" error Co-Authored-By: Claude Opus 5.5 --- packages/pg/lib/client.js | 9 +++++- packages/pg/lib/native/client.js | 9 +++++- .../test/integration/gh-issues/3852-tests.js | 28 +++++++++++++++++++ .../native/unqueryable-error-cause-tests.js | 23 +++++++++++++++ .../client/unqueryable-error-cause-tests.js | 26 +++++++++++++++++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 packages/pg/test/integration/gh-issues/3852-tests.js create mode 100644 packages/pg/test/native/unqueryable-error-cause-tests.js create mode 100644 packages/pg/test/unit/client/unqueryable-error-cause-tests.js diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 2b13c1de7..9632103e6 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -81,6 +81,7 @@ class Client extends EventEmitter { this._connected = false this._connectionError = false this._queryable = true + this._unqueryableReason = null this._activeQuery = null this._txStatus = null @@ -418,6 +419,7 @@ class Client extends EventEmitter { return this._handleErrorWhileConnecting(err) } this._queryable = false + this._unqueryableReason = this._unqueryableReason || err this._errorAllQueries(err) this.emit('error', err) } @@ -764,7 +766,12 @@ class Client extends EventEmitter { if (!this._queryable) { process.nextTick(() => { - query.handleError(new Error('Client has encountered a connection error and is not queryable'), this.connection) + query.handleError( + new Error('Client has encountered a connection error and is not queryable', { + cause: this._unqueryableReason, + }), + this.connection + ) }) return result } diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index 9ec3c8c03..3b5051d1a 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -36,6 +36,7 @@ const Client = (module.exports = function (config) { this._connecting = false this._connected = false this._queryable = true + this._unqueryableReason = null this.pipeline = Boolean(config.pipeline) this._pipelineInFlight = false @@ -110,6 +111,7 @@ Client.prototype._connect = function (cb) { // handle connection errors from the native layer self.native.on('error', function (err) { self._queryable = false + self._unqueryableReason = self._unqueryableReason || err self._errorAllQueries(err) self.emit('error', err) }) @@ -223,7 +225,11 @@ Client.prototype.query = function (config, values, callback) { if (!this._queryable) { query.native = this.native process.nextTick(() => { - query.handleError(new Error('Client has encountered a connection error and is not queryable')) + query.handleError( + new Error('Client has encountered a connection error and is not queryable', { + cause: this._unqueryableReason, + }) + ) }) return result } @@ -358,6 +364,7 @@ Client.prototype._pulsePipelinedQueryQueue = function () { // for the life of the process. self._connected = false self._queryable = false + self._unqueryableReason = self._unqueryableReason || err for (let i = 0; i < nativeQueries.length; i++) { const q = nativeQueries[i] q.native = self.native diff --git a/packages/pg/test/integration/gh-issues/3852-tests.js b/packages/pg/test/integration/gh-issues/3852-tests.js new file mode 100644 index 000000000..1619cbf3a --- /dev/null +++ b/packages/pg/test/integration/gh-issues/3852-tests.js @@ -0,0 +1,28 @@ +'use strict' +const helper = require('../test-helper') +const assert = require('assert') +const suite = new helper.Suite() + +// libpq does not report a FATAL error while no query is running +if (helper.args.native) { + return +} + +suite.test('not queryable error has the idle-in-transaction timeout as cause', async () => { + const client = new helper.pg.Client(helper.config) + const errors = [] + client.on('error', (err) => errors.push(err)) + const ended = new Promise((resolve) => client.once('end', resolve)) + await client.connect() + await client.query("SET idle_in_transaction_session_timeout = '100ms'") + await client.query('BEGIN') + await ended + + const timeoutError = errors[0] + assert.equal(timeoutError.code, '25P03') + await assert.rejects(client.query('SELECT 1'), (err) => { + assert.equal(err.message, 'Client has encountered a connection error and is not queryable') + assert.strictEqual(err.cause, timeoutError) + return true + }) +}) diff --git a/packages/pg/test/native/unqueryable-error-cause-tests.js b/packages/pg/test/native/unqueryable-error-cause-tests.js new file mode 100644 index 000000000..e9b00c02a --- /dev/null +++ b/packages/pg/test/native/unqueryable-error-cause-tests.js @@ -0,0 +1,23 @@ +'use strict' +const helper = require('./../test-helper') +const Client = require('./../../lib/native') +const suite = new helper.Suite() +const assert = require('assert') + +suite.test('not queryable error keeps the error that made the client unqueryable as cause', async function () { + const client = new Client(helper.config) + await client.connect() + + const connectionError = new Error('server closed the connection unexpectedly') + client.on('error', function (err) { + assert.strictEqual(err, connectionError) + }) + client.native.emit('error', connectionError) + + await assert.rejects(client.query('SELECT 1'), function (err) { + assert.equal(err.message, 'Client has encountered a connection error and is not queryable') + assert.strictEqual(err.cause, connectionError) + return true + }) + await client.end() +}) diff --git a/packages/pg/test/unit/client/unqueryable-error-cause-tests.js b/packages/pg/test/unit/client/unqueryable-error-cause-tests.js new file mode 100644 index 000000000..97229748f --- /dev/null +++ b/packages/pg/test/unit/client/unqueryable-error-cause-tests.js @@ -0,0 +1,26 @@ +'use strict' +const helper = require('./test-helper') +const assert = require('assert') +const suite = new helper.Suite() +const test = suite.test.bind(suite) + +test('not queryable error keeps the error that made the client unqueryable as cause', function (done) { + const client = helper.client() + client.connection.emit('readyForQuery') + + const fatal = new Error('terminating connection due to idle-in-transaction timeout') + fatal.severity = 'FATAL' + fatal.code = '25P03' + + client.on('error', function (err) { + assert.strictEqual(err, fatal) + }) + client.connection.emit('errorMessage', fatal) + + client.query('SELECT 1', function (err) { + assert.equal(err.message, 'Client has encountered a connection error and is not queryable') + assert.strictEqual(err.cause, fatal) + assert.equal(err.cause.code, '25P03') + done() + }) +})