Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

A custom socket without readyState is never closed: an expired connection leaves the pool, and sql.end() never resolves

Open Beginner friendly
#1,260 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 7 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
javascript, node.js, postgresql
Domain
backend, database

Research direction

Start with the connection shutdown logic in src/connection.js around the linked terminate() and end() lines, then inspect the custom socket tests. Reproduce with a plain Duplex stream and verify that ending the connection closes the stream and lets the pool open another socket. Done when sql.end() resolves and queries continue after max_lifetime expires.

Written by the indexing model from the issue text.

Description

The custom socket example resolves with the channel ssh2's forwardOut yields, a Duplex with no readyState. A connection never closes such a socket: terminate() sends Terminate and ends the socket only when its readyState is 'open',

https://github.com/porsager/postgres/blob/411429e7bd7a3d61155ca9a70a97c111823702ea/src/connection.js#L431

and end() then waits for a close that never comes:

https://github.com/porsager/postgres/blob/411429e7bd7a3d61155ca9a70a97c111823702ea/src/connection.js#L417

So:

  • a connection whose max_lifetime expires — 30 to 60 minutes by default — never closes and never makes room for another, so once every connection has expired, queries wait forever;
  • sql.end() never resolves, and sql.end({ timeout }) resolves with each socket still open.

Reproduction. With postgres 3.4.9, node repro.mjs (and bun repro.mjs) prints sql.end() still pending after 2 s; a duplex stream stands in for the server:

import { Duplex } from 'node:stream'
import postgres from 'postgres'

// Stands in for the server the way ssh2's forwardOut channel would: a duplex stream that
// answers the startup message and simple queries, and ends once the client ends it.
const message = (type, body) => {
  const header = Buffer.alloc(5)
  header.write(type)
  header.writeInt32BE(body.length + 4, 1)
  return Buffer.concat([header, body])
}
const ready = message('Z', Buffer.from('I'))

function server() {
  let started = false
  const stream = new Duplex({
    read() {},
    write(chunk, _, callback) {
      const reply = !started
        ? Buffer.concat([message('R', Buffer.alloc(4)), ready])
        : chunk[0] === 0x51 // Q
          ? Buffer.concat([message('C', Buffer.from('SELECT 0\0')), ready])
          : null
      started = true
      reply && setImmediate(() => stream.push(reply))
      callback()
    },
    final(callback) {
      stream.push(null)
      callback()
    }
  })
  return stream
}

let step = 'select 1'
const guard = setTimeout(() => {
  console.log(step + ' still pending after 2 s')
  process.exit(1)
}, 2000)

const sql = postgres({ max: 1, fetch_types: false, socket: server })
await sql.unsafe('select 1').simple()
step = 'sql.end()'
await sql.end()
clearTimeout(guard)
console.log('sql.end() resolved')

With max: 1, max_lifetime: 1, a query sent once the lifetime has expired stays pending the same way, and the pool never asks for a second socket.

Fix. Reading the stream's own state when a socket has no readyState closes the connection in both cases, and the pool opens the next one:

-      socket.readyState === 'open' && socket.end(b().X().end())
+      if (socket.readyState ? socket.readyState === 'open' : socket.writable)
+        socket.end(b().X().end())

net.Socket and the Deno and Cloudflare polyfills all set readyState, so they keep today's behaviour. I can open a PR with this and a test beside Custom socket that hands the connection a plain Duplex.

Dominant language
JavaScript
Stars
8.7k
Forks
389
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from porsager/postgres

All issues in porsager/postgres

Similar issues

More JavaScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.