diff --git a/index.js b/index.js index 7c0577d..5bccef0 100644 --- a/index.js +++ b/index.js @@ -88,6 +88,19 @@ function fastifyPostgres (fastify, options, next) { delete options.name const pool = new pg.Pool(options) + // 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) => { + 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') + }) + }) + const db = { connect: pool.connect.bind(pool), pool, diff --git a/test/initialization.test.js b/test/initialization.test.js index ebaf2d6..393a759 100644 --- a/test/initialization.test.js +++ b/test/initialization.test.js @@ -237,3 +237,81 @@ 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(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(4) + + const errorLogs = [] + const stream = { + write (line) { + const log = JSON.parse(line) + if (log.level === 50) errorLogs.push(log) + } + } + + const fastify = Fastify({ logger: { stream } }) + + await fastify.register(fastifyPostgres, { + connectionString + }) + await fastify.ready() + + 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 + + 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') + t.assert.strictEqual(result.rows[0].one, 1) +})