From acb33049fc5b8a28b83199962a0a2bd661d4432b Mon Sep 17 00:00:00 2001 From: Dylan Merigaud Date: Wed, 7 Oct 2026 03:38:06 +0200 Subject: [PATCH] fix(pg-cursor): clear query_timeout when the cursor finishes pg-cursor never called the Submittable callback, so the timer that Client#query starts for query_timeout was never cleared. It fired after the cursor had finished, called the callback with 'Query read timeout', and kept the cursor and the process alive until then. Call the callback once when the cursor ends or errors, as pg-query-stream already does. Fixes #3497 --- packages/pg-cursor/index.js | 12 ++++++++ packages/pg-cursor/test/query-timeout.js | 37 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 packages/pg-cursor/test/query-timeout.js diff --git a/packages/pg-cursor/index.js b/packages/pg-cursor/index.js index 5d566882b..c093c096f 100644 --- a/packages/pg-cursor/index.js +++ b/packages/pg-cursor/index.js @@ -151,6 +151,17 @@ class Cursor extends EventEmitter { this._sendRows() this.state = 'done' this.emit('end', this._result) + this._finish(null, this._result) + } + + // Call the Submittable callback the client set on this cursor, at most once. The client wraps it + // to clear its `query_timeout` timer, which otherwise fires after the cursor is done. + _finish(err, result) { + const callback = this.callback + this.callback = null + if (typeof callback === 'function') { + callback(err, result) + } } handleEmptyQuery() { @@ -185,6 +196,7 @@ class Cursor extends EventEmitter { queuedCallback.call(this, msg) } this._queue.length = 0 + this._finish(msg) if (this.listenerCount('error') > 0) { // only dispatch error events if we have a listener diff --git a/packages/pg-cursor/test/query-timeout.js b/packages/pg-cursor/test/query-timeout.js new file mode 100644 index 000000000..0097053e1 --- /dev/null +++ b/packages/pg-cursor/test/query-timeout.js @@ -0,0 +1,37 @@ +'use strict' +const assert = require('assert') +const Cursor = require('../') +const pg = require('pg') + +const text = 'SELECT generate_series as num FROM generate_series(0, 4)' +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + +describe('cursor with query_timeout', function () { + beforeEach(async function () { + this.client = new pg.Client({ query_timeout: 100 }) + await this.client.connect() + }) + + afterEach(async function () { + await this.client.end() + }) + + it('does not report a read timeout after the cursor is done', async function () { + const calls = [] + const cursor = this.client.query(new Cursor(text), (err) => calls.push(err)) + const rows = await cursor.read(100) + assert.equal(rows.length, 5) + await cursor.close() + await wait(200) + assert.deepEqual(calls, [null]) + }) + + it('does not report a read timeout after the cursor errored', async function () { + const calls = [] + const cursor = this.client.query(new Cursor('SELECT 1 / 0'), (err) => calls.push(err)) + await assert.rejects(cursor.read(1), /division by zero/) + await wait(200) + assert.equal(calls.length, 1) + assert.match(calls[0].message, /division by zero/) + }) +})