Summary
CloudflareSocket.end() calls this._cfSocket!.close() with no null guard, but _addClosedHandler() sets this._cfSocket = null as soon as the underlying socket closes. When a Pool releases a client whose backend socket was already severed (e.g. an idle connection closed by a proxy such as Cloudflare Hyperdrive, or a Postgres restart, mid-request), pool → Client.end() → CloudflareSocket.destroy() → end() dereferences null.close() and throws synchronously, crashing the in-flight request — after its transaction has already committed.
Present on master (the ! non-null assertion in end() is unsound once _cfSocket is nulled) and in published 1.3.0 / 1.4.0.
Stack trace (from production, Cloudflare Workers + Hyperdrive + drizzle)
TypeError: Cannot read properties of null (reading 'close')
at CloudflareSocket.write
at CloudflareSocket.end // this._cfSocket!.close()
at CloudflareSocket.destroy
at Client.end
at BoundPool._remove
at BoundPool._release
at Client.release // releasing the client after the txn
The transaction succeeded; only the cleanup (client release → socket teardown) throws, so the request 500s even though the work committed.
Relevant source (packages/pg-cloudflare/src/index.ts)
end(data = Buffer.alloc(0), encoding = 'utf8', callback = () => {}) {
this.write(data, encoding, (err) => {
this._cfSocket!.close() // <-- _cfSocket may be null here
if (callback) callback(err)
})
return this
}
_addClosedHandler() {
this._cfSocket!.closed.then(() => {
if (!this._upgrading) {
this._cfSocket = null // <-- nulled on close
this.emit('close')
} ...
})
}
Proposed fix
Guard the teardown: this._cfSocket?.close(). If the socket is already closed there is nothing to close, and end()/destroy() should be a safe no-op rather than throw. Happy to send a PR.
Workaround
Patching end() to use optional chaining (this._cfSocket?.close()) via a package patch resolves it.
Summary
CloudflareSocket.end()callsthis._cfSocket!.close()with no null guard, but_addClosedHandler()setsthis._cfSocket = nullas soon as the underlying socket closes. When aPoolreleases a client whose backend socket was already severed (e.g. an idle connection closed by a proxy such as Cloudflare Hyperdrive, or a Postgres restart, mid-request),pool→Client.end()→CloudflareSocket.destroy()→end()dereferencesnull.close()and throws synchronously, crashing the in-flight request — after its transaction has already committed.Present on
master(the!non-null assertion inend()is unsound once_cfSocketis nulled) and in published1.3.0/1.4.0.Stack trace (from production, Cloudflare Workers + Hyperdrive + drizzle)
The transaction succeeded; only the cleanup (client release → socket teardown) throws, so the request 500s even though the work committed.
Relevant source (
packages/pg-cloudflare/src/index.ts)Proposed fix
Guard the teardown:
this._cfSocket?.close(). If the socket is already closed there is nothing to close, andend()/destroy()should be a safe no-op rather than throw. Happy to send a PR.Workaround
Patching
end()to use optional chaining (this._cfSocket?.close()) via a package patch resolves it.