Repository: nock/nock (2025-06-29T10:02:40Z)

Issue:
Timeout when using jest "useFakeTimers" functionality
**What is the expected behavior?**

When mocking timers in jest I would expect mocks to pass properly as those (to me) shouldn't be related to network communication.

```
jest
  .useFakeTimers('modern')
  .setSystemTime(new Date('2020-01-01').getTime())
```

**What is the actual behavior?**

Test case hangs until the whole suite times out

**Possible solution**
Make mocks not reliant on the passing time, or maybe provide a flag that would handle this behaviour differently?

**How to reproduce the issue**

```
import nock from 'nock'
import axios from 'axios'

jest
  .useFakeTimers('modern')
  .setSystemTime(new Date('2020-01-01').getTime())

describe('Test', () => {
  const url = 'http://www.google.com'

  
  beforeEach(() => nock(url).get('/').reply(201))
  afterEach(() => expect(nock.isDone()).toEqual(true))
  
  it('should pass this test', async () => {
    const res = await axios(url)
    expect(res.status).toEqual(201)
  })
})
```

Remove fake timers and the test case passes

**Does the bug have a test case?**

Yes, provided above

**Versions**

| Software   | Version(s) |
| ---------- | ---------- |
| Nock       | v13.0.11 |
| Node       | v14.16.0 |
| TypeScript | v3.9.7 |



Discussion:
This happens to me right after upgrading **Jest** from **v26** to **v27**, @sdomagala are you in the same condition as well?
I've read that `jest` changed something with its timers API with the 27 release. My guess is that it interferes with async processes / timers that nock is using internally.
By specifying `jest.useFakeTimers('legacy')` (instead of default `modern`) would mitigate the hanging for me.

Quote from Jest blog post on **v27** release [1]:

> Another default that we are changing affects Fake Timers aka Timer Mocks. We introduced an opt-in "modern" implementation of Fake Timers in Jest 26 accessed transparently through the same API, but with much more comprehensive mocking, such as for Date and queueMicrotask.
This modern fake timers implementation will now be the default. If you are among the unlucky few who are affected by the subtle implementation differences too heavily to migrate, you can get back the old implementation using jest.useFakeTimers("legacy") or, if you are enabling fake timers globally via configuration, "timers": "legacy".

---
1: https://jestjs.io/blog/2021/05/25/jest-27
@imcotton for me it happened the moment I've tried to use those modern timers and function `setSystemTime`. I believe it was first introduced in jest 26.

Using legacy timer indeed resolves the issue in this test case but it doesn't let you set global time so it doesn't really help me.

[Per docs](https://jestjs.io/docs/jest-object#jestsetsystemtimenow-number--date):
```
Note: This function is only available when using modern fake timers implementation
```
Recently I [experienced this](https://github.com/PaperStrike/Pjax/commit/148003a00bd48fecc7d0327a18e3d619863dfedc#diff-7b321abe055d2f56e34a1978a9bfe35232ef26fbda2ebf1779df27576dd5e566R165-R166) too. It seems like ~nock uses "greater than" to test if the current time has passed the defined delay time (0 if not set explicitly), and respond only when the test assessed true.~ As Jest's modern version timer locks, nock will not respond.

Try to advance at somewhere:

```js
jest.useFakeTimers();
const url = 'https://example';
nock(url).get('/').reply(201);

const axiosPromise = axios(url);

// Advance 1ms for nock delay.
jest.advanceTimersByTime(1);

// Got the response now.
const res = await axiosPromise;
expect(res.status).toBe(201);
```
if you could digg into that and see if we could change the implementation to something more resilient that will work with jest's new fake timers that'd be great
https://github.com/nock/nock/blob/3efd7382011a8d1ca084bfc4043a332a796a0618/lib/intercepted_request_router.js#L66-L69

Here, Jest's modern fake timer completely locks the time including the tick, so the connect is never reached, and a [`Jest.runAllTicks`](https://github.com/facebook/jest/blob/b5aa00204e80927efee6b44ac7dd6b22b8c78e22/website/versioned_docs/version-27.0/JestObjectAPI.md#jestrunallticks) is enough.
https://github.com/facebook/jest/issues/10221
Also broken with sinon.useFakeTimers. The first mocked calls works, but any subsequent call times out.
Add an eye here, bcs I just faced a same issue, with pretty much the same codes as above. But in my case, using `jest.useFakeTimers('legacy')` does not solve the problem.

#### Versions:
"jest": "26.6.3",
"nock": "12.0.3",
"typescript": "4.4.2",
"node": "14.18.1",
I've tried the recently added `doNotFake` option (`jest.useFakeTimers({ doNotFake: ['nextTick'] })`) but it didn't work for me. Has anyone had any success with that new [option](https://jestjs.io/docs/timer-mocks#selective-faking)?
Using `setImmediate` (`jest.useFakeTimers({ doNotFake: ['setImmediate'] })`) worked for me (I'm using `request` from `supertest`)
If you're still having issues, try adding both `nextTick` and `setImmediate` to the array, this is the only way it'd work for me.

`jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate'] }).setSystemTime(new Date('2022-01-01'))`
> 

yeah, that's true, before I found this answer, I tried one by one to not fake this functionality
```js
            'Date',
            'hrtime',
            'performance',
            'queueMicrotask',
            'requestAnimationFrame',
            'cancelAnimationFrame',
            'requestIdleCallback',
            'cancelIdleCallback',
            'clearImmediate',
            'setInterval',
            'clearInterval',
            'setTimeout',
            'clearTimeout',
            'nextTick',
            'setImmediate',
```

and found out to making fakeTimers working is to not faking "'nextTick', 'setImmediate'" but I still don't know what effect If I do that on my test 😓 

> If you're still having issues, try adding both `nextTick` and `setImmediate` to the array, this is the only way it'd work for me.
> 
> `jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate'] }).setSystemTime(new Date('2022-01-01'))`

This worked for me, thanks!
I followed @chornos13 comment, started with the whole list into `doNotFake`, and removed one by one elements. The setup which is working on my computer (my setup is jest and supertest) is:

```ts
import { jest } from "@jest/globals";

jest
  .useFakeTimers({
    doNotFake: [
      "nextTick",
      "setImmediate",
      "clearImmediate",
      "setInterval",
      "clearInterval",
      "setTimeout",
      "clearTimeout",
    ],
  })
  .setSystemTime(new Date("2022-02-15"));
```

I don't understand why Jest doesn't offer a way to just change the date without mocking the universe…
Changing `beforeEach` statement to `beforeAll` statement worked for me. (jest version 27.x)
None of these worked for me
> Using `setImmediate` (`jest.useFakeTimers({ doNotFake: ['setImmediate'] })`) worked for me (I'm using `request` from `supertest`)

Fantastic, this fixed it for me! I'm using `supertest` also.

When an error was thrown inside an async route handler in my Express app, and I used `jest.useFakeTimers()`, the error was not being reported and the test timed out. Your solution fixed it, and now errors are successfully thrown and reported without the test timing out.

`doNotFake: ['nextTick']` had no effect, but `doNotFake: ['setImmediate']` did the trick. Thanks so much!
I wrote a small function to fix this issue by extending `jest.useFakeTimers` options with a new `fake` option.
```typescript
/**
 * Adds `fake` option to `jest.useFakeTimers` config api
 *
 * @param config Fake timers config options
 *
 * @return Jest instance
 */
function useFakeTimers(config?: FakeTimersConfig & { fake?: FakeableAPI[] }) {
  if (config?.fake) {
    if (config.doNotFake) {
      throw new Error('Passing both `fake` and `doNotFake` options to `useFakeTimers()` is not supported.')
    }
    
    const { fake, ...options } = config
    return jest.useFakeTimers({
      ...options,
      doNotFake: Array<FakeableAPI>(
        'Date',
        'hrtime',
        'nextTick',
        'performance',
        'queueMicrotask',
        'requestAnimationFrame',
        'cancelAnimationFrame',
        'requestIdleCallback',
        'cancelIdleCallback',
        'setImmediate',
        'clearImmediate',
        'setInterval',
        'clearInterval',
        'setTimeout',
        'clearTimeout',
      ).filter((api) => !fake.includes(api)),
    })
  }

  return jest.useFakeTimers(config)
}
```
### Usage
```typescript
  beforeAll(async () => {
    useFakeTimers({ fake: ['Date'] })
  })
```
cc @NTag
The same issue also appears in vitest (as they use the same timer, but the doNotFake property doesnt exist there).
Solved it there by using the following config:
```typescript
vi.useFakeTimers({
    shouldAdvanceTime: true,
    toFake: ["Date"],
});
```

Instead of 

```js
jest.useFakeTimers('legacy')
```

it should be

```js
jest.useFakeTimers({
    legacyFakeTimers: true
})
```
https://github.com/testing-library/user-event/issues/833 might be your issue if you are using [testing library user events](https://testing-library.com/docs/user-event/intro/). Took me forever to figure out but this eventually fixed it for me:

```js
jest.useFakeTimers({ advanceTimers: true });
```
i had to add { advanceTimers: true } to useFakeTimers like this:

jest.useFakeTimers({ advanceTimers: true }).setSystemTime(dateAfterHour);

also i had to delete this from jest config:

   fakeTimers: {
     enableGlobally: true,
   },
   
   adding advanceTimers;true to global config did not help 
  note: i m using jest 29 without nock
   
In the context of [fastify inject](https://fastify.dev/docs/v1.14.x/Documentation/Testing/) I found that it was essential to use the `advanceTimers: true` config to avoid timeouts.

```ts
beforeAll(async () => {
    jest
      .useFakeTimers({ advanceTimers: true })
      .setSystemTime(new Date('2022-04-24'));
  });

  afterAll(async () => {
    jest.useRealTimers();
  });
```

I'm not a `fastify` person, but I'm guessing this is because the test `inject` relies on time advancing in order to return its response. 


> In the context of [fastify inject](https://fastify.dev/docs/v1.14.x/Documentation/Testing/) I found that it was essential to use the `advanceTimers: true` config to avoid timeouts.
> 
> ```ts
> beforeAll(async () => {
>     jest
>       .useFakeTimers({ advanceTimers: true })
>       .setSystemTime(new Date('2022-04-24'));
>   });
> 
>   afterAll(async () => {
>     jest.useRealTimers();
>   });
> ```
> 
> I'm not a `fastify` person, but I'm guessing this is because the test `inject` relies on time advancing in order to return its response.

This works for me, thanks! (Nestjs Fastify Application) :+1: 
> In the context of [fastify inject](https://fastify.dev/docs/v1.14.x/Documentation/Testing/) I found that it was essential to use the `advanceTimers: true` config to avoid timeouts.
> 
> beforeAll(async () => {
>     jest
>       .useFakeTimers({ advanceTimers: true })
>       .setSystemTime(new Date('2022-04-24'));
>   });
> 
>   afterAll(async () => {
>     jest.useRealTimers();
>   });
> I'm not a `fastify` person, but I'm guessing this is because the test `inject` relies on time advancing in order to return its response.

Any similar workaround for Vitest? It doesn't seem to support this property...
@eyalyst We use basically the same command in vitest:

```javascript
        vi.useFakeTimers({
            now: Date.now(),
            shouldAdvanceTime: true,
            toFake: ["Date"],
        });
```
@WtfJoke I'll try that - thanks!
:tada: This issue has been resolved in version 14.0.7 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/nock/v/14.0.7)
- [GitHub release](https://github.com/nock/nock/releases/tag/v14.0.7)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:



Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index f482cdc01..5236d17ae 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -22,6 +22,10 @@ overrides:
       mocha: true
     rules:
       mocha/no-exclusive-tests: 'error'
+  - files:
+      - 'tests_jest/**/*.js'
+    globals:
+      jest: true
 
 rules:
   # TODO These are included in standard and should be cleaned up and turned on.
diff --git a/README.md b/README.md
index 01d5f4a5a..54bdc58d6 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ For instance, if a module performs HTTP requests to a CouchDB server or makes HT
   - [Requests made by ES Modules are not intercepted](#requests-made-by-es-modules-are-not-intercepted)
   - [Axios](#axios)
   - [Memory issues with Jest](#memory-issues-with-jest)
+  - [Fake timers](#fake-timers)
 - [Debugging](#debugging)
 - [Contributing](#contributing)
 - [Contributors](#contributors)
@@ -1613,6 +1614,86 @@ One of the core principles of [Jest](https://jestjs.io/) is that it runs tests i
 It does this by manipulating the modules cache of Node in a way that conflicts with how Nock monkey patches the builtin `http` and `https` modules.
 [Related issue with more details](https://github.com/nock/nock/issues/1817).
 
+### Fake timers
+
+### Jest
+
+To use `nock` in conjunction with `jest` fake timers, make sure you're using the "async" functions when advancing the
+timers, such as `jest.advanceTimersByTimeAsync()` or `jest.runAllTimersAsync()`. Otherwise, the timers will not be
+advanced correctly and you'll experience a timeout in your tests.
+
+```js
+test('should mock a request with fake timers', async () => {
+  jest.useFakeTimers()
+
+  const scope = nock('https://example.com')
+    .get('/path')
+    .delay(1000)
+    .reply(200, 'response')
+
+  // Simulate a request
+  const request = got('https://example.com/path')
+
+  // Fast-forward time
+  await jest.advanceTimersByTimeAsync(1000)
+
+  // Or advance all timers
+  await jest.runAllTimersAsync()
+
+  // Wait for the request to complete
+  const response = await request
+
+  expect(response.body).toBe('response')
+  jest.useRealTimers() // Restore real timers after the test
+  scope.done()
+})
+```
+
+In case you don't need testing delays, you can instruct `jest` to advance the timers automatically using the
+`advanceTimers` option
+
+```js
+jest.useFakeTimers({ advanceTimers: true })
+```
+
+### Sinon
+
+In a similar way to `jest`, if you are using `sinon` fake timers, you should use the `clock.tickAsync()` or
+`clock.runAllAsync()` methods to advance the timers correctly.
+
+```js
+it('should us sinon timers', async () => {
+  clock = sinon.useFakeTimers()
+  const scope = nock('https://example.com')
+    .get('/path')
+    .delay(1000)
+    .reply(200, 'response')
+
+  // Simulate a request
+  const request = got('https://example.com/path')
+
+  // Fast-forward time
+  await clock.tickAsync(1000)
+
+  // Or run all timers
+  await clock.runAllAsync()
+
+  // Wait for the request to complete
+  const response = await request
+
+  expect(response.body).toBe('response')
+  clock.restore()
+  scope.done()
+})
+```
+
+Same applies for `sinon`, if you don't need testing delays, you can instruct `sinon` to advance the timers automatically
+using the `shouldAdvanceTime` option
+
+```js
+clock = sinon.useFakeTimers({ shouldAdvanceTime: true })
+```
+
 ## Debugging
 
 Nock uses node internals [`debuglog`](https://nodejs.org/api/util.html#utildebuglogsection-callbackg), so just run with environmental variable `NODE_DEBUG` set to `nock:*`.
diff --git a/lib/common.js b/lib/common.js
index 312bf1c26..0d8035ffc 100644
--- a/lib/common.js
+++ b/lib/common.js
@@ -1,7 +1,6 @@
 'use strict'
 
 const { common: debug } = require('./debug')
-const timers = require('timers')
 const url = require('url')
 const util = require('util')
 const http = require('http')
@@ -521,24 +520,36 @@ function deepEqual(expected, actual) {
 const timeouts = new Set()
 const immediates = new Set()
 
-const wrapTimer =
-  (timer, ids) =>
-  (callback, ...timerArgs) => {
-    const cb = (...callbackArgs) => {
-      try {
-        // eslint-disable-next-line n/no-callback-literal
-        callback(...callbackArgs)
-      } finally {
-        ids.delete(id)
-      }
+const _setImmediate = (callback, ...timerArgs) => {
+  const cb = (...callbackArgs) => {
+    try {
+      // eslint-disable-next-line n/no-callback-literal
+      callback(...callbackArgs)
+    } finally {
+      immediates.delete(id)
+    }
+  }
+
+  const id = setImmediate(cb, 0, ...timerArgs)
+
+  immediates.add(id)
+  return id
+}
+
+const _setTimeout = (callback, ...timerArgs) => {
+  const cb = (...callbackArgs) => {
+    try {
+      // eslint-disable-next-line n/no-callback-literal
+      callback(...callbackArgs)
+    } finally {
+      timeouts.delete(id)
     }
-    const id = timer(cb, ...timerArgs)
-    ids.add(id)
-    return id
   }
 
-const setTimeout = wrapTimer(timers.setTimeout, timeouts)
-const setImmediate = wrapTimer(timers.setImmediate, immediates)
+  const id = setTimeout(cb, ...timerArgs)
+  timeouts.add(id)
+  return id
+}
 
 function clearTimer(clear, ids) {
   ids.forEach(clear)
@@ -714,8 +725,8 @@ module.exports = {
   percentDecode,
   percentEncode,
   removeAllTimers,
-  setImmediate,
-  setTimeout,
+  setImmediate: _setImmediate,
+  setTimeout: _setTimeout,
   stringifyRequest,
   convertFetchRequestToClientRequest,
 }


Test patch:
diff --git a/tests/got/test_fake_timer.js b/tests/got/test_fake_timer.js
index 620bb1ecd..f473c659d 100644
--- a/tests/got/test_fake_timer.js
+++ b/tests/got/test_fake_timer.js
@@ -9,7 +9,11 @@ it('should still return successfully when fake timer is enabled', async () => {
   const clock = fakeTimers.install()
   const scope = nock('http://example.test').get('/').reply()
 
-  await got('http://example.test')
+  const promise = got('http://example.test')
+
+  await clock.runAllAsync() // Run all fake timers to ensure all scheduled tasks are executed
+
+  await promise
 
   clock.uninstall()
   scope.done()
diff --git a/tests/got/test_reply_headers.js b/tests/got/test_reply_headers.js
index 9432b7a27..081487579 100644
--- a/tests/got/test_reply_headers.js
+++ b/tests/got/test_reply_headers.js
@@ -427,7 +427,7 @@ describe('`replyDate()`', () => {
       const scope = nock('http://example.test').replyDate().get('/').reply()
 
       const req = got('http://example.test/')
-      clock.tick()
+      await clock.runAllAsync()
       const { headers } = await req
       const date = new Date()
       expect(headers).to.include({ date: date.toUTCString() })
diff --git a/tests/test_fake_timers.js b/tests/test_fake_timers.js
new file mode 100644
index 000000000..358bfc220
--- /dev/null
+++ b/tests/test_fake_timers.js
@@ -0,0 +1,108 @@
+'use strict'
+
+const sinon = require('sinon')
+const { expect } = require('chai')
+const nock = require('..')
+
+describe('test fake timers (sinon)', () => {
+  const url = 'https://api.example.com'
+
+  let scope
+  let clock
+
+  describe('using timers to test delays', () => {
+    beforeEach(() => {
+      clock = sinon.useFakeTimers()
+
+      scope = nock(url)
+        .get('/api/v1/resource')
+        .times(3)
+        .reply(
+          429,
+          { message: 'Too many requests' },
+          { 'content-type': 'application/json' },
+        )
+        .get('/api/v1/resource')
+        .reply(
+          200,
+          { message: 'Success' },
+          { 'content-type': 'application/json' },
+        )
+    })
+
+    afterEach(() => {
+      clock.restore()
+      nock.cleanAll()
+      nock.restore()
+    })
+
+    it('correctly trigger the timers', async () => {
+      const fetch = createRetryFetch({ retries: 3, delay: 100 })
+
+      const request = fetch(new URL('/api/v1/resource', url))
+
+      await clock.tickAsync(100) // first request
+      await clock.tickAsync(100) // first retry
+      await clock.tickAsync(100) // second retry
+      await clock.tickAsync(100) // third retry
+
+      const response = await request.then(response => response.json())
+
+      expect(response).to.be.deep.equal({ message: 'Success' })
+
+      scope.done()
+    })
+  })
+
+  describe('advance timers workaround', () => {
+    beforeEach(() => {
+      clock = sinon.useFakeTimers({ shouldAdvanceTime: true })
+
+      scope = nock(url)
+        .get('/api/v1/resource')
+        .reply(
+          200,
+          { message: 'Success' },
+          { 'content-type': 'application/json' },
+        )
+    })
+
+    afterEach(() => {
+      clock.restore()
+      nock.cleanAll()
+      nock.restore()
+    })
+
+    it('should use fake timers', async () => {
+      const request = fetch(new URL('/api/v1/resource', url))
+
+      const response = await request.then(response => response.json())
+
+      expect(response).to.be.deep.equal({ message: 'Success' })
+
+      scope.done()
+    })
+  })
+})
+
+function createRetryFetch(options = {}) {
+  const { retries = 3, delay = 100 } = options
+
+  return async function fetchWithRetry(url, fetchOptions = {}) {
+    let currentAttempt = 0
+
+    while (currentAttempt <= retries) {
+      const response = await fetch(url, fetchOptions)
+
+      if (!response.ok) {
+        await new Promise(resolve => setTimeout(resolve, delay))
+        currentAttempt++
+        continue
+      }
+
+      return response // Success or non-retryable error
+    }
+
+    throw new Error('Fetch request failed after multiple retries.')
+  }
+}
diff --git a/tests_jest/test_fake_timers.spec.js b/tests_jest/test_fake_timers.spec.js
new file mode 100644
index 000000000..14856f42c
--- /dev/null
+++ b/tests_jest/test_fake_timers.spec.js
@@ -0,0 +1,117 @@
+'use strict'
+
+const {
+  describe,
+  it,
+  beforeEach,
+  afterEach,
+  afterAll,
+} = require('@jest/globals')
+const { expect } = require('chai')
+const nock = require('..')
+
+describe('test fake timers (jest)', () => {
+  const url = 'https://api.example.com'
+
+  afterAll(() => {
+    nock.restore()
+  })
+
+  describe('using timers to test delays', () => {
+    let scope
+
+    beforeEach(() => {
+      jest.useFakeTimers()
+
+      scope = nock(url)
+        .get('/api/v1/resource')
+        .delay(1000)
+        .reply(
+          429,
+          { message: 'Too many requests' },
+          { 'content-type': 'application/json' },
+        )
+        .get('/api/v1/resource')
+        .reply(
+          200,
+          { message: 'Success' },
+          { 'content-type': 'application/json' },
+        )
+    })
+
+    afterEach(() => {
+      nock.cleanAll()
+      jest.useRealTimers()
+    })
+
+    it('correctly trigger the timers', async () => {
+      const fetch = createRetryFetch({ retries: 3, delay: 100 })
+
+      const request = fetch(new URL('/api/v1/resource', url))
+
+      await jest.runAllTimersAsync() // first request
+      await jest.runAllTimersAsync() // first retry
+      await jest.runAllTimersAsync() // second retry
+      await jest.runAllTimersAsync() // third retry
+
+      const response = await request.then(async response => response.json())
+
+      expect(response).to.be.deep.equal({ message: 'Success' })
+
+      scope.done()
+    })
+  })
+
+  describe('advance timers workaround', () => {
+    let scope
+
+    beforeEach(() => {
+      jest.useFakeTimers({ advanceTimers: true })
+
+      scope = nock(url)
+        .get('/api/v1/resource')
+        .reply(
+          200,
+          { message: 'Success' },
+          { 'content-type': 'application/json' },
+        )
+    })
+
+    afterEach(() => {
+      nock.cleanAll()
+      jest.useRealTimers()
+    })
+
+    it('should use fake timers', async () => {
+      const request = fetch(new URL('/api/v1/resource', url))
+
+      const response = await request.then(response => response.json())
+
+      expect(response).to.be.deep.equal({ message: 'Success' })
+
+      scope.done()
+    })
+  })
+})
+
+function createRetryFetch(options = {}) {
+  const { retries = 3, delay = 100 } = options
+
+  return async function fetchWithRetry(url, fetchOptions = {}) {
+    let currentAttempt = 0
+
+    while (currentAttempt <= retries) {
+      const response = await fetch(url, fetchOptions)
+
+      if (!response.ok) {
+        await new Promise(resolve => setTimeout(resolve, delay))
+        currentAttempt++
+        continue
+      }
+
+      return response // Success or non-retryable error
+    }
+
+    throw new Error('Fetch request failed after multiple retries.')
+  }
+}