Skip to content

Close the connection when fetching array types fails - #1231

Open
Nitjsefnie wants to merge 2 commits into
porsager:masterfrom
Nitjsefnie-OSC:fix-types-fetch-failure
Open

Nitjsefnie wants to merge 2 commits into
porsager:masterfrom
Nitjsefnie-OSC:fix-types-fetch-failure

Conversation

@Nitjsefnie

Copy link
Copy Markdown

Summary

A failed array-type lookup on a new connection left a rejected promise that no one handled, because ReadyForQuery discarded the promise returned by fetchArrayTypes(). The Node process then exited. The connection was also handed to the pool with no array types. With this change, the failure is settled on the connection itself: the waiting query or reserve is rejected with the server's error, and the connection is closed, so the next connection fetches its types again.

Related Issues and Pull Requests

Fixes #1192
Depends on #1230

Changes

  • src/connection.js fetchArrayTypes(reserve): wraps the types query's reject, matching the resolve wrapper in Store fetched array types before running the first query #1230. On failure it sets needsTypes again and rejects a waiting reserve. ReadyForQuery passes the reserve in because it removes the reserve from initial before the fetch starts, so errored() never reaches it. The query is awaited through .catch(), so no rejection is left without a handler.
  • ReadyForQuery: a connection that still needs its types is closed (if (needsTypes) return terminate()) instead of being opened. This happens synchronously, before onopen() can give the connection to queued work. errored() still rejects the waiting query with the database error, as before.
  • tests/index.js: a withoutArrayTypes(fn) helper connects as a role that cannot read pg_catalog.pg_type, through psql, like the other superuser steps. Its finally restores the grant first. It records unhandledRejection for the duration of the test, except on Deno, which has no such hook and aborts on an unhandled rejection. Three cases use the helper:
    • a first query must reject with 42501 without an unhandled rejection, and a later sql.array query must succeed once the grant is restored;
    • two concurrent queries on a max: 1 client must both reject with 42501;
    • a waiting sql.reserve() must reject with 42501.
  • cjs/, cf/, deno/: regenerated with npm run build, keeping only this change's hunks. A fresh build also rewrites output from 411429e and 9afd16d that was never regenerated; that output is left out of this PR.

Testing

  • All three new cases fail on Store fetched array types before running the first query #1230 alone and pass with this change.
  • Mutants of the fix:
    • dropping the .catch() leaves an unhandled 42501;
    • dropping the close guard makes the later array query fail with 22P02;
    • moving the close into the .catch() continuation makes the second queued query fail with CONNECTION_DESTROYED;
    • dropping the reserve rejection makes the reserve case hang until it times out.
  • The Deno leg fails on the defect with an uncaught permission denied for table pg_type, and passes with the fix.
  • The full npm test suite (esm, cjs, deno) passed, with all 268 tests, on Node 24 against PostgreSQL 17, set up like CI. On a heavily loaded host, some full runs failed in the listen tests and in a 500 ms timing test. Store fetched array types before running the first query #1230 without this change failed the same way on the same host, and the listen client runs with fetch_types: false, which never reaches this code.

Follow-ups / Known Limitations

  • This PR includes Store fetched array types before running the first query #1230's commit because both changes edit fetchArrayTypes(). Once Store fetched array types before running the first query #1230 merges, only the second commit remains.
  • Behaviour change: while the types fetch keeps failing, every query gets its own connection attempt and fails with the server's error. Before this change, the connection opened without types, and the process crashed on the discarded promise.
  • fetchState(), which runs the target_session_attrs query, still wraps only resolve. So a failing state query still ends in an unhandled rejection. That path runs only on servers that do not report in_hot_standby at startup; it reproduced on PostgreSQL 13.

Footer

Generated by Claude Opus 5.5 (brief, implementation, review)

Nitjsefnie and others added 2 commits September 26, 2026 12:33
fetchArrayTypes() awaited the types query and only then filled
typeArrayMap. The types query's ReadyForQuery resolves it and, in the
same synchronous call, goes on to execute(initial), so a new
connection's first query was built while the map was still empty and
an sql.array() parameter was bound as its element type, e.g.
"malformed array literal" with an array cast. Fill the map inside the
query's resolve, like fetchState() does, so it is populated before the
initial query is built.

Fixes porsager#789

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadyForQuery discarded the promise returned by fetchArrayTypes(), so
when the types query failed, e.g. a role that cannot read pg_type or a
statement_timeout cancelling it, its rejection went unhandled and
terminated the process. errored() had already rejected the waiting
query, and the connection then went on to onopen() without array types,
so the next sql.array() query on it was bound as its element type. A
reserve waiting on the connection was even handed that connection.

Settle the failure synchronously in the types query's reject instead: it
marks the types as still needed and rejects a waiting reserve with the
same error. ReadyForQuery then closes the connection rather than opening
it, so queued queries go to a new connection that fetches the types
again, and the awaited query is caught since nothing is left for it to
report.

Fixes porsager#1192

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetchArrayTypes() promise is discarded in ReadyForQuery: a failing fetch_types query crashes the process via unhandled rejection

1 participant