From 30719eeea0722b192eb9d1c83ccd567fce21f2ef Mon Sep 17 00:00:00 2001 From: TemoSulava Date: Sun, 18 Oct 2026 21:48:45 +0400 Subject: [PATCH 1/3] fix: log idle client errors instead of crashing Fixes #168 --- index.js | 5 +++++ test/initialization.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/index.js b/index.js index 7c0577d..0334619 100644 --- a/index.js +++ b/index.js @@ -88,6 +88,11 @@ function fastifyPostgres (fastify, options, next) { delete options.name const pool = new pg.Pool(options) + // stop idle client errors (e.g. a server restart) from crashing the process + pool.on('error', (err) => { + fastify.log.error({ err }, 'idle client error in pg pool') + }) + const db = { connect: pool.connect.bind(pool), pool, diff --git a/test/initialization.test.js b/test/initialization.test.js index ebaf2d6..bf73122 100644 --- a/test/initialization.test.js +++ b/test/initialization.test.js @@ -237,3 +237,38 @@ test('const result = await fastify.pg and a fastify.pg custom namespace should e t.assert.deepStrictEqual(result.rowCount, 1) t.assert.deepStrictEqual(resultOne.rowCount, 1) }) + +test('Should log instead of crashing when an idle client errors', { timeout: 10000 }, async (t) => { + t.plan(3) + + let onPoolErrorLog + const poolErrorLogged = new Promise((resolve) => { onPoolErrorLog = resolve }) + const stream = { + write (line) { + const log = JSON.parse(line) + if (log.msg === 'idle client error in pg pool') onPoolErrorLog(log) + } + } + + const fastify = Fastify({ logger: { stream } }) + t.after(() => fastify.close()) + + await fastify.register(fastifyPostgres, { + connectionString + }) + await fastify.ready() + + const { rows } = await fastify.pg.query('SELECT pg_backend_pid() AS pid') + + const admin = new fastify.pg.Client({ connectionString }) + await admin.connect() + await admin.query('SELECT pg_terminate_backend($1)', [rows[0].pid]) + await admin.end() + + const log = await poolErrorLogged + t.assert.strictEqual(log.level, 50) + t.assert.strictEqual(log.err.code, '57P01') + + const result = await fastify.pg.query('SELECT 1 AS one') + t.assert.strictEqual(result.rows[0].one, 1) +}) From d77a178fa8a018e58df1ce0cc76236a419cd164e Mon Sep 17 00:00:00 2001 From: TemoSulava Date: Tue, 20 Oct 2026 20:40:39 +0400 Subject: [PATCH 2/3] fix: handle client errors on connect and absorb pool re-emits --- index.js | 10 +++++++--- test/initialization.test.js | 22 +++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 0334619..59a6bf2 100644 --- a/index.js +++ b/index.js @@ -88,9 +88,13 @@ function fastifyPostgres (fastify, options, next) { delete options.name const pool = new pg.Pool(options) - // stop idle client errors (e.g. a server restart) from crashing the process - pool.on('error', (err) => { - fastify.log.error({ err }, 'idle client error in pg pool') + // pg-pool re-emits idle client errors here; unhandled, it crashes the process + pool.on('error', () => {}) + // fires for idle and checked-out clients, so log only here + pool.on('connect', (client) => { + client.on('error', (err) => { + fastify.log.error({ err, name }, 'pg client error') + }) }) const db = { diff --git a/test/initialization.test.js b/test/initialization.test.js index bf73122..4c11ce9 100644 --- a/test/initialization.test.js +++ b/test/initialization.test.js @@ -238,36 +238,40 @@ test('const result = await fastify.pg and a fastify.pg custom namespace should e t.assert.deepStrictEqual(resultOne.rowCount, 1) }) -test('Should log instead of crashing when an idle client errors', { timeout: 10000 }, async (t) => { +test('Should log instead of crashing when a checked-out client errors', { timeout: 10000 }, async (t) => { t.plan(3) - let onPoolErrorLog - const poolErrorLogged = new Promise((resolve) => { onPoolErrorLog = resolve }) + const errorLogs = [] const stream = { write (line) { const log = JSON.parse(line) - if (log.msg === 'idle client error in pg pool') onPoolErrorLog(log) + if (log.level === 50) errorLogs.push(log) } } const fastify = Fastify({ logger: { stream } }) - t.after(() => fastify.close()) await fastify.register(fastifyPostgres, { connectionString }) await fastify.ready() - const { rows } = await fastify.pg.query('SELECT pg_backend_pid() AS pid') + const client = await fastify.pg.connect() + t.after(() => { + client.release() + return fastify.close() + }) + const { rows } = await client.query('SELECT pg_backend_pid() AS pid') + const ended = new Promise((resolve) => client.once('end', resolve)) const admin = new fastify.pg.Client({ connectionString }) await admin.connect() await admin.query('SELECT pg_terminate_backend($1)', [rows[0].pid]) await admin.end() + await ended - const log = await poolErrorLogged - t.assert.strictEqual(log.level, 50) - t.assert.strictEqual(log.err.code, '57P01') + t.assert.strictEqual(errorLogs[0]?.err.code, '57P01') + await t.assert.rejects(client.query('SELECT 1'), /not queryable/) const result = await fastify.pg.query('SELECT 1 AS one') t.assert.strictEqual(result.rows[0].one, 1) From ca7a549b1ec2be2eff360a975230cb4db4798a50 Mon Sep 17 00:00:00 2001 From: TemoSulava Date: Thu, 22 Oct 2026 20:35:02 +0400 Subject: [PATCH 3/3] fix: log only the first error per client and test the idle-client path --- index.js | 4 ++++ test/initialization.test.js | 43 +++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 59a6bf2..5bccef0 100644 --- a/index.js +++ b/index.js @@ -92,7 +92,11 @@ function fastifyPostgres (fastify, options, next) { pool.on('error', () => {}) // fires for idle and checked-out clients, so log only here pool.on('connect', (client) => { + let errored = false client.on('error', (err) => { + // a dropped connection emits follow-ups like 'Connection terminated unexpectedly'; like pg-pool, log only the first + if (errored) return + errored = true fastify.log.error({ err, name }, 'pg client error') }) }) diff --git a/test/initialization.test.js b/test/initialization.test.js index 4c11ce9..393a759 100644 --- a/test/initialization.test.js +++ b/test/initialization.test.js @@ -238,8 +238,46 @@ test('const result = await fastify.pg and a fastify.pg custom namespace should e t.assert.deepStrictEqual(resultOne.rowCount, 1) }) +test('Should log instead of crashing when an idle client errors', { timeout: 10000 }, async (t) => { + t.plan(4) + + const errorLogs = [] + const stream = { + write (line) { + const log = JSON.parse(line) + if (log.level === 50) errorLogs.push(log) + } + } + + const fastify = Fastify({ logger: { stream } }) + t.after(() => fastify.close()) + + await fastify.register(fastifyPostgres, { + connectionString + }) + await fastify.ready() + + const client = await fastify.pg.connect() + const { rows } = await client.query('SELECT pg_backend_pid() AS pid') + const ended = new Promise((resolve) => client.once('end', resolve)) + client.release() + t.assert.strictEqual(fastify.pg.pool.idleCount, 1) + + const admin = new fastify.pg.Client({ connectionString }) + await admin.connect() + await admin.query('SELECT pg_terminate_backend($1)', [rows[0].pid]) + await admin.end() + await ended + + t.assert.strictEqual(errorLogs.length, 1) + t.assert.strictEqual(errorLogs[0].err.code, '57P01') + + const result = await fastify.pg.query('SELECT 1 AS one') + t.assert.strictEqual(result.rows[0].one, 1) +}) + test('Should log instead of crashing when a checked-out client errors', { timeout: 10000 }, async (t) => { - t.plan(3) + t.plan(4) const errorLogs = [] const stream = { @@ -270,7 +308,8 @@ test('Should log instead of crashing when a checked-out client errors', { timeou await admin.end() await ended - t.assert.strictEqual(errorLogs[0]?.err.code, '57P01') + t.assert.strictEqual(errorLogs.length, 1) + t.assert.strictEqual(errorLogs[0].err.code, '57P01') await t.assert.rejects(client.query('SELECT 1'), /not queryable/) const result = await fastify.pg.query('SELECT 1 AS one')