Error during a `rows`-limited query permanently wedges the connection — `handleError` sends no Sync
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 55/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 冷清
- 技术栈
- javascript, postgresql
调研方向
阅读 lib/query.js,尤其是 Query._getRows()、handleError()、handleCommandComplete() 和 handleEmptyQuery();对比 packages/pg-cursor/index.js。运行提供的 rows 模式复现,并添加回归覆盖,验证 ErrorResponse 后跟随 ReadyForQuery,且后续查询能够 resolve,同时包含可能的 double-sync edge,供 maintainer 审查。
由索引模型根据 Issue 内容生成。
描述
- pg: 8.22.0
- Node: 24.14.1
- PostgreSQL: 18.4 (macOS; not server-specific — the wedge follows from the wire protocol)
Summary
When a query runs with the rows option (portal suspension — client.query({ text, rows: N })),
an ErrorResponse from the server permanently wedges the connection: the query itself rejects
correctly, but ReadyForQuery never arrives, client.readyForQuery stays false, and every
subsequent query on that client queues forever.
Found while developing a wire-protocol bridge, where the message flow made the missing
frame visible.
Reproduction
const { Client } = require('pg')
async function main() {
const client = new Client()
await client.connect()
// Errors mid-portal: division by zero on row 5, fetched in pages of 2.
await client
.query({ text: 'select 10 / (5 - i) as v from generate_series(1, 7) g(i)', rows: 2 })
.catch((err) => console.log('query rejected:', err.message)) // "division by zero" — fine
// The connection is now wedged: this never settles.
const result = await Promise.race([
client.query('select 1').then(() => 'follow-up resolved'),
new Promise((r) => setTimeout(() => r('follow-up HUNG'), 5000)),
])
console.log(result) // "follow-up HUNG"
}
main()
Root cause
With rows set, Query.prepare() drives the extended protocol with Flush, not Sync:
_getRows() sends Execute(rows=N) + connection.flush(), and again on every
PortalSuspended; Sync is only sent from handleCommandComplete / handleEmptyQuery
(lib/query.js). When the server answers with ErrorResponse instead, it enters
ignore-till-sync — it discards everything until a Sync arrives and withholds
ReadyForQuery. handleError sends nothing, so the RFQ never comes and the client's
queue is stuck permanently.
Some history, because the code carries a fossil of the old behavior: handleError did
send Sync from at least 2014 (// need to sync after error during a prepared statement
— the comment is still in today's source). The Oct 2020 pipelining redesign (d31486fb,
then dd3ce616) moved Sync into _getRows(), pipelined right after Execute, and removed
the handleError call — correct for the normal prepared path, where a Sync is already
in flight by the time an error arrives. But the rows branch of _getRows() pipelines
Flush instead, so rows-mode was left with no error-path Sync at all; only the comment
survived.
pg-cursor — which uses the same Execute+Flush model — deliberately kept its
handleError sync for exactly this reason (packages/pg-cursor/index.js:
// call sync to trigger a readyForQuery), which is why cursors recover from errors
and the core rows path does not.
Suggested fix
Mirror what handleCommandComplete / handleEmptyQuery already do for rows-mode:
handleError(err, connection) {
// rows-mode pipelines Flush, not Sync (_getRows) — after an ErrorResponse the
// backend discards messages until Sync, so send it here or ReadyForQuery never
// arrives and the connection is wedged.
if (this.rows) {
connection.sync()
}
// ... existing body unchanged
}
One edge worth a maintainer's judgement: 9c678e10 (Oct 2020) guarded the old
sync-after-error because PostgreSQL 9.x could send both CommandComplete and
ErrorResponse for a single timed-out query. If that pairing is still in scope,
handleCommandComplete would already have sent Sync and the line above would
double-send (two RFQs — the class of bug #2420 fixed in pg-cursor). A small
this._syncSent flag shared by the three handlers would make it airtight.
I searched for an existing report and found none covering this path — the closest are
#549 (a crash in the pre-2020 handleError sync call) and #1500 (readyForQuery stuck
after a dead connection). Since rows is undocumented, this is a low-visibility
surface — but the wedge is silent and permanent when hit.
- 主要语言
- JavaScript
- 星标
- 13.2k
- 派生
- 1.4k
- 平均合并
- 6 天 15 小时
- 30 天内合并 PR
- 6
贡献指南
这个仓库没有索引到贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
brianc/node-postgres 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 68/100
brianc/node-postgres#3770 ·
-
难度 2/5 1-3 小时 新手友好度 75/100
brianc/node-postgres#3716 · 1 条评论 ·
-
难度 2/5 1-3 小时 新手友好度 68/100
brianc/node-postgres#3631 · 1 条评论 ·
-
难度 1/5 1-3 小时 新手友好度 62/100
brianc/node-postgres#2857 ·
-
难度 1/5 1 小时以内 新手友好度 68/100
brianc/node-postgres#2433 ·
查看 brianc/node-postgres 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 65/100
antfu-collective/icones#398 ·
-
ECmail.com 未关闭
难度 1/5 1 小时以内 新手友好度 90/100
wesbos/burner-email-providers#554 ·
-
难度 2/5 1-3 小时 新手友好度 65/100
radiantearth/stac-browser#1023 ·
-
难度 2/5 1-3 小时 新手友好度 75/100
-
难度 2/5 1-3 小时 新手友好度 75/100
components-web-app/docs#92 ·