jest icon indicating copy to clipboard operation
jest copied to clipboard

TLSWRAP open handle wrongly detected with MongoDB connections

Open lblanch opened this issue 2 years ago • 17 comments

💥 Regression Report

Running Jest with --detectOpenHandles detects a TLSWRAP potential open handle on MongoDb connections, even if they are properly closed in an "afterAll" enclosure and jest exits without issues. The exact message is:

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  TLSWRAP

       6 |
       7 | beforeAll(async () => {
    >  8 |   await client.connect()
         |                ^
       9 | })
      10 |
      11 | afterAll(async () => {

      at Object.resolveSRVRecord (node_modules/mongodb/src/connection_string.ts:69:7)
      at Object.connect (node_modules/mongodb/src/operations/connect.ts:51:12)
      at node_modules/mongodb/src/mongo_client.ts:410:7
      at Object.maybePromise (node_modules/mongodb/src/utils.ts:618:3)
      at MongoClient.connect (node_modules/mongodb/src/mongo_client.ts:409:12)
      at app.test.js:8:16
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:387:19)
      at _run10000 (node_modules/@jest/core/build/cli/index.js:408:7)
      at runCLI (node_modules/@jest/core/build/cli/index.js:261:3)

This seems to happen with MongoDb's official NodeJs driver, and, by extension, any other packages that use that driver (in my case I got it with mongoose and connect-mongo-session)

Last working version

Worked up to version: 26.6.3

Stopped working in version: 27.0.0

To Reproduce

NOTE: A valid MongoDB URI is needed to reproduce this issue.

Within an empty directory:

  1. Create a new empty Node project with:
npm init 
  1. Install jest and mongodb
npm install jest mongodb
  1. Create a app.test.js file with following contents (add a valid MongoDB URI where necessary):
const { MongoClient } = require("mongodb");

// Replace the uri string with your MongoDB connection string.
const uri ='YOUR-MONGODB-URI'
const client = new MongoClient(uri)

beforeAll(async () => {
  await client.connect()
})

afterAll(async () => {
  await client.close()
})

test('testing', () => {
  expect(client).toBeDefined()
})
  1. Run the test with jest and --detectOpenHandles
jest --detectOpenHandles

Expected behavior

No open handles should be found.

Link to repl or repo (highly encouraged)

Use the code snippet provided above.

Run npx envinfo --preset jest

Paste the results here:

System:
    OS: macOS 11.4
    CPU: (8) arm64 Apple M1
  Binaries:
    Node: 16.4.2 - /opt/local/bin/node
    npm: 7.19.1 - /opt/local/bin/npm
  npmPackages:
    jest: ^27.0.0 => 27.0.6 

Thank you for your help! <3

lblanch avatar Jul 15 '21 14:07 lblanch

I can confirm having exactly the same issue with Jest 27.0.4.

dimaip avatar Jul 20 '21 03:07 dimaip

I'm having the same issue as well

clinzy25 avatar Jul 30 '21 18:07 clinzy25

I was having the same problem and blaming Jest or the mongodb node driver.

The reason for the problem was that I wasn't actually dealing with a singleton like I was lead to believe: even though I was following the text-book standard for connection pooling in an application, each file ran by Jest was creating a new connection when doing an import of my getConnection function that was supposed to access the singleton stored in a module-level variable. I am not sure why Jest interpreted N-times my db.js file, but it did (while N=number of test files).

I suggest to verify if the same problem is happening to you; my solution so far has been to add an afterAll with db.close() in my topmost describe blocks in each test file. The afterAll closes each connection opened (same as I was doing in the jest-teardown level).

Now my jest runs close without forcing and no open handles are left.

Eitz avatar Sep 23 '21 00:09 Eitz

I detected a similar issue using the ioredis package within the beforeEach function.

// src/helper/redis.ts
async function withRedis(handle: (r: IORedis.Redis) => Promise<void>): Promise<void> {
  const redis = new Redis(Number(process.env.REDIS_PORT), process.env.REDIS_HOSTNAME, {
    password: process.env.REDIS_PASSWORD,
    tls: true as any,
  });

  try { 
    await handle(redis);
  } finally {
    await redis.quit();
  }
}

// src/some.spec.ts
beforeEach(async () => {
  await withRedis(async redis => {
    const keys = await redis.keys(`*${process.env.KEY_PREFIX}*`);
    for (const key of keys) {
      await redis.del(key);
    }
  });
});

It produces sporadically the following warning with --detectOpenHandles.

Jest has detected the following 2 open handles potentially keeping Jest from exiting:

  ●  TLSWRAP

      13 |
      14 | async function withRedis(handle: (r: IORedis.Redis) => Promise<void>): Promise<void> {
    > 15 |   const redis = new Redis(Number(process.env.REDIS_PORT), process.env.REDIS_HOSTNAME, {
         |                 ^
      16 |     password: process.env.REDIS_PASSWORD,
      17 |     tls: true as any,
      18 |   });

      at node_modules/ioredis/built/connectors/StandaloneConnector.js:48:21
      at StandaloneConnector.connect (node_modules/ioredis/built/connectors/StandaloneConnector.js:47:16)
      at node_modules/ioredis/built/redis/index.js:272:55
      at Redis.Object.<anonymous>.Redis.connect (node_modules/ioredis/built/redis/index.js:251:21)
      at new Redis (node_modules/ioredis/built/redis/index.js:160:14)
      at withRedis (src/helper/redis.ts:15:17)
      at Object.<anonymous> (src/some.spec.ts:22:11)

sschmeck avatar Sep 29 '21 07:09 sschmeck

I have the same issue with something as simple as this using axios

import axios from "axios";

it("runs without open handles", async () => {
  await axios.get("https://jsonplaceholder.typicode.com/todos/1");
  expect(true).toBe(true);
});

"axios": "^0.21.4", "jest": "^27.2.4", "ts-jest": "^27.0.5"

vollmerr avatar Sep 29 '21 18:09 vollmerr

The problem seems to be that the Async hooks module in Node doesn't emit a destroy event for the "TLSWRAP" hook type.

In Jest we have today something like:

if (type === 'PROMISE' ||
    type === 'TIMERWRAP' ||
    type === 'ELDHISTOGRAM' ||
    type === 'PerformanceObserver' ||
    type === 'RANDOMBYTESREQUEST' ||
    type === 'DNSCHANNEL' ||
    type === 'ZLIB'
  ) return;

These are all problematic hook types that are simply ignored. And "TLSWRAP" should probably be added to this list.

(Since there is always an underlying socket object which is what we're really interested in.)

But to test this, we'd have to set up a TLS session object which is not exactly trivial.

malthe avatar Nov 05 '21 14:11 malthe

Any news on this? Stumbled over this problem just now and desperately trying to find a solution for it...

dornfeder avatar Nov 30 '21 06:11 dornfeder

@dornfeder I have success with simply adding a test in the snippet above for "TLSWRAP". But to test that out is a lot of CI/CD work setting up a certificate, etc.

malthe avatar Nov 30 '21 06:11 malthe

Great catch @malthe! I can confirm that adding TLSWRAP to the ignore list in collectHandles seems to get rid of the issue for MongoDB connections. Would be nice to get some eyes on this.

odziem avatar Mar 21 '22 12:03 odziem

Great catch @malthe! I can confirm that adding TLSWRAP to the ignore list in collectHandles seems to get rid of the issue for MongoDB connections. Would be nice to get some eyes on this.

@odziem Can you please explain how to do that? I did not find it in jest documentation and still wanna fix the problem.

valerii15298 avatar Jun 01 '22 06:06 valerii15298

I face similar error it was solve by removing --detectOpenHandles,

ruthcyg avatar Jun 11 '22 11:06 ruthcyg

@ruthcyg but that is arguably the wrong solution.

malthe avatar Jun 11 '22 18:06 malthe

Guys any updates here?

mkovel avatar Jul 12 '22 23:07 mkovel

Having the same issue with Axios

prma85 avatar Jul 18 '22 21:07 prma85

still have same issue here.

any updates?

myothuko98 avatar Aug 08 '22 07:08 myothuko98

The problem seems to be that the Async hooks module in Node doesn't emit a destroy event for the "TLSWRAP" hook type.

@malthe would you say that's a bug in node? Or should we just ignore the type?

SimenB avatar Aug 08 '22 07:08 SimenB

@SimenB it's some time since I started the thread, but I think it's just a bug in jest really:

And "TLSWRAP" should probably be added to this list.

malthe avatar Aug 08 '22 07:08 malthe

Im having same issues, there is nothing else I can do to awai the mongoose.connect or mongoDB.connect async. Plese let us know if there is a workaround.

UriConMur avatar Aug 21 '22 03:08 UriConMur

I'm having the same problem while doing:

const user = {
  userName: "lionardo",
  password: "freeSabana",
};

describe("Given the route /register", () => {
  describe("When the method is POST", () => {
    test("Then a valid endpoint should have been sent a 201 code and registered a user", async () => {
      await User.deleteMany();
      const response = await request(app).post("/users/register").send(user);
      expect(response.statusCode).toEqual(201);
    });
  });
});

The error is:

`Jest has detected the following 1 open handle potentially keeping Jest from exiting:

● TCPWRAP

  23 |     });
  24 |
> 25 |     mongoose.connect(mongoUrl, (error) => {
     |              ^
  26 |       if (error) {
  27 |         debug(
  28 |           chalk.bgRed.white(`

I have tried so many things but without any result.

I think the problem is that database connection isn't really closed. I tried: if (process.env.NODE_ENV === 'test') { mongoose.connection.close(); }); }

For some reason NODE_ENV is dev, despite that the documentation states that it's expected to be test.

Closing database connection immediately on application start may cause problems in units that actually use database connection. As explained in the guide, MongoDB connection should be at the end of test. Since default Mongoose connection is used, it can be:

afterAll(() => mongoose.disconnect());

Anything of those worked for me, but I think the solution is close. If anyone see anything with that please let us know.

aronilie avatar Aug 21 '22 10:08 aronilie

I am also experiencing this issue, but for me it is caused by node-fetch. As far as I can see none of the workarounds in this thread are applicable, and I don't think I'm doing anything wrong, e.g. missing an await. Is there any way I can get my tests to pass?

roblframpton avatar Sep 28 '22 19:09 roblframpton

I have a much simpler reproduction for this issue:

const tls = require('tls');

describe('Test', () => {
  it("runs a test", () => {
      const socket = new tls.TLSSocket();
      socket.destroy();
  });
});

Prints:

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  TLSWRAP

      3 | describe('Test', () => {
      4 |   it("runs a test", () => {
    > 5 |       const socket = new tls.TLSSocket();
        |                      ^
      6 |       socket.destroy();
      7 |   });
      8 | });

      at Object.<anonymous> (index.test.ts:5:22)

pimterry avatar Oct 07 '22 15:10 pimterry

node --expose-gc .\node_modules\jest-cli\bin\jest.js

const tls = require("tls");
describe("Test", () => {
  it("runs a test", async () => {
    let socket = new tls.TLSSocket();
    socket.destroy();

    await new Promise(r => setTimeout(r, 0));
    // @ts-ignore
    gc();
  });
});

This seems to do the trick, but I'm not sure how I should open the PR in jest. Looks like it's better left to you to decide. cc @SimenB

liuxingbaoyu avatar Oct 07 '22 21:10 liuxingbaoyu

https://github.com/facebook/jest/releases/tag/v29.2.0

SimenB avatar Oct 14 '22 09:10 SimenB

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

github-actions[bot] avatar Nov 14 '22 00:11 github-actions[bot]