Skip to content

fix(pglite-socket): don't touch the database when detaching an already-detached handler - #1173

Open
hsheth2 wants to merge 1 commit into
electric-sql:mainfrom
hsheth2:fix/pglite-socket-detach-after-stop
Open

hsheth2 wants to merge 1 commit into
electric-sql:mainfrom
hsheth2:fix/pglite-socket-detach-after-stop

Conversation

@hsheth2

@hsheth2 hsheth2 commented Oct 29, 2026

Copy link
Copy Markdown

Fixes #1172

Problem

If a client's socket closes in the same event-loop turn as await server.stop() followed by await db.close(), the server later throws an unhandled TypeError: Cannot read properties of undefined (reading '_IsTransactionBlock'). The socket's close listener defers handleClose() with setImmediate. stop() detaches the same handler and resolves, and db.close() finishes. Then the deferred handleClose() calls detach() again. detach() calls clearTransactionIfNeeded(), and so db.isInTransaction(), before it checks whether the handler is still attached. handleClose() doesn't await detach(), so the error is an unhandled rejection.

The README example and the pglite-server CLI exit the process right after db.close(), so they never run the deferred callback. Test suites that start and stop a server per file do run it.

Change

detach() returns before it touches the query queue or the database when the handler has no socket. The existing check after the await stays, because a second detach can finish while the first one waits for a ROLLBACK.

Tests

  • Added tests/stop-then-close.test.ts. It starts the stop() and db.close() shutdown from the server-side socket's close event, which makes the race deterministic, and asserts that no unhandled rejection follows. It fails on main with the _IsTransactionBlock TypeError and passes with this change, 5 out of 5 repeated runs.
  • tsc, eslint ./src ./tests --max-warnings 0 and prettier --check ./src ./tests are clean.
  • I couldn't build the PGlite WASM locally, so I ran the socket package's tests against the published @electric-sql/pglite@0.5.13. In that setup, 7 tests in index.test.ts, query-with-node-pg.test.ts, query-with-postgres-js.test.ts and extended-query-error.test.ts fail on main both with and without this change. They need workspace builds I didn't have. All the other tests pass, including connection-cleanup.test.ts.

This change was made with AI assistance. The tests above were run locally before opening this PR.

…y-detached handler

A handler's socket close listener defers handleClose() with setImmediate.
If stop() detaches that handler first and the caller then closes the
database, the deferred handleClose() runs detach() again, which calls
db.isInTransaction() on the closed database before it checks whether the
handler is still attached. handleClose() doesn't await detach(), so the
TypeError is an unhandled rejection.

Return from detach() before touching the queue or the database when the
handler has no socket.

@tdrz tdrz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

This branch has not been deployed

No deployments
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.

[BUG]: pglite-socket: a socket close queued before stop() reads the closed database after db.close()

2 participants