Skip to content

pg-cloudflare: CloudflareSocket.end() throws null.close() when releasing a client whose socket already closed #3689

@hkirste

Description

@hkirste

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), poolClient.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions