From 75e14f14d6c733fd0d2c1e76a619618a973e6a49 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 4 Sep 2026 23:17:48 -0400 Subject: [PATCH] fix(pglite): never leave a client without ReadyForQuery When the backend produced no bytes for a request, or a reply without ReadyForQuery, the bridge forwarded it as-is and the client waited for ever while the session was free. On roy this froze the UI: a pooled connection hung right after its handshake and every request queued in the pool behind it. Incomplete replies now get a synthesized error plus ReadyForQuery, and the bridge logs them, so a glitch becomes a visible SQL error that the pool can retry. --- db/pglite/pglite.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/db/pglite/pglite.go b/db/pglite/pglite.go index bf780be14..432252ce3 100644 --- a/db/pglite/pglite.go +++ b/db/pglite/pglite.go @@ -468,7 +468,11 @@ func (pg *PGlite) handleConn(conn net.Conn, ioBase string) { if !handshakeDone { replies, handshakeDone = pg.fixHandshake(replies) } else { - replies = ensureReadyForQuery(replies, trapErr) + fixed := ensureReadyForQuery(replies, trapErr) + if len(fixed) != len(replies) { + fmt.Fprintf(pg.cfg.Stderr, "# bridge: incomplete reply to %s (trap=%v); synthesized error+ReadyForQuery\n", wireTags(packet), trapErr != nil) + } + replies = fixed } if trapErr != nil && pg.trace { fmt.Fprintf(pg.cfg.Stderr, "# bridge: trap recovered: %v\n", strings.SplitN(trapErr.Error(), "\n", 2)[0]) @@ -766,9 +770,6 @@ func (pg *PGlite) fixHandshake(replies [][]byte) ([][]byte, bool) { // which would otherwise leave the client waiting forever. func ensureReadyForQuery(replies [][]byte, trapErr error) [][]byte { data := bytes.Join(replies, nil) - if len(data) == 0 && trapErr == nil { - return replies - } sawError, sawReady := false, false for rest := data; len(rest) >= 5; { n := int(rest[1])<<24 | int(rest[2])<<16 | int(rest[3])<<8 | int(rest[4]) @@ -783,14 +784,18 @@ func ensureReadyForQuery(replies [][]byte, trapErr error) [][]byte { } rest = rest[n+1:] } - if trapErr != nil && !sawError && !sawReady { - replies = append(replies, errorResponse("XX000", "pglite trap: "+strings.SplitN(trapErr.Error(), "\n", 2)[0])) - sawError = true + if sawReady { + return replies } - if sawError && !sawReady { - return append(replies, []byte{'Z', 0, 0, 0, 5, 'I'}) + // A reply without ReadyForQuery would leave the client waiting forever; fail it instead. + if !sawError { + msg := "pglite: incomplete reply from the backend" + if trapErr != nil { + msg = "pglite trap: " + strings.SplitN(trapErr.Error(), "\n", 2)[0] + } + replies = append(replies, errorResponse("XX000", msg)) } - return replies + return append(replies, []byte{'Z', 0, 0, 0, 5, 'I'}) } func errorResponse(code, msg string) []byte {