Skip to content

client.end() never resolves in Deno when using SSL connections #3420

Closed
@SylvainMarty

Description

@SylvainMarty

Hello there,

First of all, thank you for your work on pg, I used it extensively and it's a great library!

I am currently working on a project with Deno 2, pg, and a database hosted on Neon Postgres.

I ran into a weird issue when calling await client.end():

error: Top-level await promise never resolved
// await client.end();
// ^

This error only happens when connecting to a database using the sslmode=require option.

I had no trouble working on the project with a local Postgres instance.

Neon Postgres only supports SSL connections so this problem is kind of blocking for me.

How to reproduce

I created a repository with minimal code to reproduce the error: https://github.com/SylvainMarty/pg-deno-ssl-issue.

You can also copy-paste the following code in a Deno 2 project:

// @ts-types="npm:@types/pg@8.11.11"
import pg from "npm:pg@8.14.1";

const client = new pg.Client({
  connectionString:
    // CHANGE THIS TO YOUR OWN NEON DB
    "postgresql://user:password@your-own-neon-domain.eu-central-1.aws.neon.tech/neondb?sslmode=require",
});

await client.connect();
await client.query(`SELECT 1`);
await client.end();
// This last line will throw an error:
// error: Top-level await promise never resolved
// await client.end();
// ^

Investigation

Since I was curious, I dug deeper and I realized the error is happening because the event end is never emitted by the Connection class declared in connection.js:

this.stream.on('close', function () {
self.emit('end')
})

To be sure, I added a log in the end() method of the connection.js file:

  end() {
    // 0x58 = 'X'
    this._ending = true
    if (!this._connecting || !this.stream.writable) {
      this.stream.end()
      return
    }
    return this.stream.write(endBuffer, () => {
+     console.log(
+       "event listeners",
+       EventEmitter.getEventListeners(this.stream, 'end'),
+       EventEmitter.getEventListeners(this.stream, 'close'),
+     );
      this.stream.end()
    })
  }

And ran the code again, this time with the log enabled:

// Output from the Node.js runtime:
event listeners [ [Function: onReadableStreamEnd], [Function (anonymous)] ] [ [Function: onSocketCloseDestroySSL] ]

// Output from the Deno runtime:
event listeners [ [Function: onReadableStreamEnd], [] ]

In deed, the close event listener is never called because it doesn't exist.

Why this happens

This problem is due to the fact that Deno's compatibility layer with the Node.js TLS API is not 100% complete yet.

I found the following test suites which are still in the TODO file of the Node compat project:

  • parallel/test-tls-close-error.js
  • parallel/test-tls-close-event-after-write.js
  • parallel/test-tls-close-notify.js

You've read it right, the close event is not emitted like in NodeJS.

I have tried to make those tests pass in Deno/Node compatibility project but without success. There's actually a lot of work to be done on Deno's side for even being able to run those tests correctly, even before actually fixing the issue at hand (Deno doesn't support self signed certificates and that's how NodeJS expects those tests to be running).

I am planning to open an issue on Deno's repo and see if I can help advance on this subject but from what I've seen, the issue is linked to multiple problems and this might not be fixed anytime soon.

Moving forward

Yes I know, everything points to this being more of a Deno scope than pg's.

Now, I think pg can be updated to work around this issue.

Actually, the workaround I found was as simple as this change in pg/lib/connection.js:

  end() {
    // 0x58 = 'X'
    this._ending = true
    if (!this._connecting || !this.stream.writable) {
      this.stream.end()
+     if (this.ssl && Deno?.version) {
+       this.emit('end')
+     }
      return
    }
    return this.stream.write(endBuffer, () => {
      this.stream.end()
+     if (this.ssl && Deno?.version) {
+       this.emit('end')
+     }
    })
  }

I've tested this workaround against the minimal reproducible example provided above, and it successfully resolves the issue. The client.end() call completes properly when using SSL connections to Neon Postgres from Deno.

I'm offering this as a tentative solution and am open to alternative approaches if you have concerns about this implementation.

I saw your comment on the issue #2225 and I understand supporting new runtimes is not a priority for you. My primary goal is to enable Deno users to work with pg and Neon (or other SSL-required Postgres instances) without hitting this blocking issue.

Since pg is a building block for many libraries used extensively by the JS ecosystem (to quote a few: TypeORM, Slonik, pg-boss), I really think it would be great for everyone if this library worked well with Deno 2.

I personally had no trouble working with the pg library on Deno until I found this issue (my project has extensive integration tests and pg is doing a great job at everything we throw at it).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions