From a2588b3073b5bd125f0fb3b419bdc9f2e8524291 Mon Sep 17 00:00:00 2001 From: Xavier Geerinck Date: Wed, 2 Sep 2026 12:28:53 +0200 Subject: [PATCH] Guard nextWrite against a closed socket - fixes #1208 closed() nulls the socket and only reconnects on a later timer, so a connection can sit with no socket across ticks. nextWrite() was the one consumer of socket in this file without a guard - terminate() has `if (socket)` and end() has `socket && ...` - and it is also the one reached from the 'data' handler, where a throw has no query to reject and escapes as an uncaughtException that takes the process down. A pooled connection hides this because the pool rotates the dead connection away. A reserved one is pinned and cannot be rotated, so every subsequent write on it is fatal. Settle the pending queries rather than just dropping the write: returning false alone leaves the caller awaiting a write that never happens. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012njhLokwkVZMjV7KcHsbA5 --- src/connection.js | 10 ++++++++++ tests/index.js | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/connection.js b/src/connection.js index 10ab1bb3..45626ac2 100644 --- a/src/connection.js +++ b/src/connection.js @@ -252,6 +252,16 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose } function nextWrite(fn) { + if (!socket) { + // closed() nulls the socket and reconnect() only recreates it on a later + // timer. write() is also reached from the 'data' handler, so a throw here + // has no query to reject and escapes as an uncaughtException. Settle the + // pending queries rather than dropping the write, or the caller hangs. + nextWriteTimer !== null && clearImmediate(nextWriteTimer) + chunk = nextWriteTimer = null + error(Errors.connection('CONNECTION_CLOSED', options, socket)) + return false + } const x = socket.write(chunk, fn) nextWriteTimer !== null && clearImmediate(nextWriteTimer) chunk = nextWriteTimer = null diff --git a/tests/index.js b/tests/index.js index 845c6ce0..2851c782 100644 --- a/tests/index.js +++ b/tests/index.js @@ -2707,6 +2707,32 @@ t('Ensure reserve on query throws proper error', async() => { ] }) +t('Writing to a closed reserved connection rejects instead of crashing', async() => { + let downstream + const proxy = net.createServer(x => { + downstream = x + const upstream = net.connect(5432, '127.0.0.1') + x.pipe(upstream).pipe(x) + x.on('error', () => upstream.destroy()) + upstream.on('error', () => x.destroy()) + }) + + await new Promise(r => proxy.listen(0, r)) + + const sql = postgres({ ...options, host: '127.0.0.1', port: proxy.address().port, max: 1 }) + , reserved = await sql.reserve() + + await reserved`select 1` + downstream.end() + await delay(50) + + const code = await reserved`select 1`.catch(e => e.code) + reserved.release() + proxy.close() + + return ['CONNECTION_CLOSED', code] +}) + t('query during copy error', async() => { const sql = postgres(options) // eslint-disable-line await sql`create table test (id serial primary key, name text)`