Repository: nock/nock
Pull request #2033 (2020-07-01 00:19:31)

fix(intercept): mark Interceptors consumed immediately
When an request is matched to an Interceptor, mark it consumed immediately so
that similar requests made in parallel don't get matched incorrectly.

This changes the behavior of aborted requests slightly.
Previously, an Interceptor was marked consumed just prior to the "response" event.
Now it will be marked consumed just prior to the "finished" event.
The change can only be noticed if a request is aborted while in a "finished" listener.

There might be some debate on whether this is a braking change or a bug fix, but I'm inclined to release this as a patch considering v13 was just published. 

Fixes: #1937 
fyi @marian-r & @gurpreetatwal 


Patch:
diff --git a/lib/intercepted_request_router.js b/lib/intercepted_request_router.js
index 9ee24be..27631ff 100644
--- a/lib/intercepted_request_router.js
+++ b/lib/intercepted_request_router.js
@@ -295,6 +295,9 @@ class InterceptedRequestRouter {
         'interceptor identified, starting mocking'
       )
 
+      matchedInterceptor.markConsumed()
+      matchedInterceptor.req = req
+
       // wait to emit the finish event until we know for sure an Interceptor is going to playback.
       // otherwise an unmocked request might emit finish twice.
       req.emit('finish')
diff --git a/lib/playback_interceptor.js b/lib/playback_interceptor.js
index 56fc0bd..9fcc7c6 100644
--- a/lib/playback_interceptor.js
+++ b/lib/playback_interceptor.js
@@ -124,8 +124,6 @@ function playbackInterceptor({
   const { logger } = interceptor.scope
 
   function start() {
-    interceptor.markConsumed()
-    interceptor.req = req
     req.headers = req.getHeaders()
 
     interceptor.scope.emit('request', req, interceptor, requestBodyString)


Test patch:
diff --git a/tests/test_abort.js b/tests/test_abort.js
index 7844486..67b1b84 100644
--- a/tests/test_abort.js
+++ b/tests/test_abort.js
@@ -122,6 +122,9 @@ describe('`ClientRequest.abort()`', () => {
     }, 10)
   })
 
+  // The Interceptor is considered consumed just prior to the `finish` event on the request,
+  // all tests below assert the Scope is done.
+
   it('Emits the expected event sequence when aborted inside a `finish` event listener', done => {
     const scope = nock('http://example.test').get('/').reply()
 
@@ -146,14 +149,11 @@ describe('`ClientRequest.abort()`', () => {
         'error',
         'close',
       ])
-      expect(scope.isDone()).to.be.false()
+      scope.done()
       done()
     }, 10)
   })
 
-  // The Interceptor is considered consumed just prior to the `response` event on the request,
-  // all tests below assert the Scope is done.
-
   it('Emits the expected event sequence when aborted after a delay from the `finish` event', done => {
     // use the delay functionality to create a window where the abort is called during the artificial connection wait.
     const scope = nock('http://example.test').get('/').delay(100).reply()
diff --git a/tests/test_intercept_parallel.js b/tests/test_intercept_parallel.js
new file mode 100644
index 0000000..b6427dc
--- /dev/null
+++ b/tests/test_intercept_parallel.js
@@ -0,0 +1,64 @@
+'use strict'
+
+const { expect } = require('chai')
+const nock = require('..')
+const got = require('./got_client')
+
+require('./setup')
+
+// Tests for a regression where multiple ClientRequests call `.end` during the
+// same event loop iteration. https://github.com/nock/nock/issues/1937
+
+describe('interception in parallel', () => {
+  const origin = 'https://example.test'
+  const makeRequest = () =>
+    got(origin)
+      .then(res => res.statusCode)
+      .catch(reason => {
+        if (reason.code === 'ERR_NOCK_NO_MATCH') return 418
+        throw reason
+      })
+
+  it('consumes multiple requests, using multiple Interceptors on the same Scope', async () => {
+    const scope = nock(origin)
+
+    scope.get('/').reply(200)
+    scope.get('/').reply(201)
+
+    const results = await Promise.all([
+      makeRequest(),
+      makeRequest(),
+      makeRequest(),
+    ])
+
+    expect(results.sort()).to.deep.equal([200, 201, 418])
+    scope.done()
+  })
+
+  it('consumes multiple requests, using a single Interceptor', async () => {
+    const scope = nock(origin).get('/').times(2).reply(200)
+
+    const results = await Promise.all([
+      makeRequest(),
+      makeRequest(),
+      makeRequest(),
+    ])
+
+    expect(results.sort()).to.deep.equal([200, 200, 418])
+    scope.done()
+  })
+
+  it('consumes multiple requests, using multiple Scopes', async () => {
+    nock(origin).get('/').reply(200)
+    nock(origin).get('/').reply(201)
+
+    const results = await Promise.all([
+      makeRequest(),
+      makeRequest(),
+      makeRequest(),
+    ])
+
+    expect(results.sort()).to.deep.equal([200, 201, 418])
+    expect(nock.isDone()).to.equal(true)
+  })
+})