Repository: nock/nock (js)

Issue:
delayBody doesn't work with abort signal (nock@14.0.0-beta.16)
### Please avoid duplicates

- [X] I checked [all open bugs](https://github.com/nock/nock/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and none of them matched my problem.

### Reproducible test case

https://gist.github.com/sam-super/1706b4e46a92a9d3ec64a13ee7c7d3ca

### Nock Version

14.0.0-beta.16

### Node Version

v22.11.0

### TypeScript Version

_No response_

### What happened?

Unlike nock v13 (with node-fetch package) `delayBody` doesn't work correctly with `14.0.0-beta.16` using node's native fetch.

[this gist](https://gist.github.com/sam-super/1706b4e46a92a9d3ec64a13ee7c7d3ca) compares using nock vs spinning up a nodejs server to delay the response body. if you run it you should get this:

```text
node version: v22.11.0
 ---------- node test ----------
response received after 0.01s
NODE: Correctly threw error after timeout: TimeoutError: The operation was aborted due to timeout
 ---------- /node test ---------
 ---------- nock test ----------
response received after 0.008s
body received after 1.209s
NOCK: BUG - got a successful response in spite of setting AbortSignal.timeout(1000) and response delay of 1200: NockResBody
 ---------- /nock test ---------
 ```
in case it helps `delayConnection` works in `14.0.0-beta.16` (however in 14.0.0-beta.7 `delayConnection` didn't work).

### Would you be interested in contributing a fix?

- [X] yes

Pull request description:
fix(fetch): add support for aborting requests using AbortSignal
closes #2808

Patch:
diff --git a/lib/create_response.js b/lib/create_response.js
index 8d0c030..fd953ee 100644
--- a/lib/create_response.js
+++ b/lib/create_response.js
@@ -16,8 +16,9 @@ const responseStatusCodesWithoutBody = [204, 205, 304]
 
 /**
  * @param {import('http').IncomingMessage} message
+ * @param {AbortSignal} signal
  */
-function createResponse(message) {
+function createResponse(message, signal) {
   const responseBodyOrNull = responseStatusCodesWithoutBody.includes(
     message.statusCode || 200,
   )
@@ -27,6 +28,7 @@ function createResponse(message) {
           message.on('data', chunk => controller.enqueue(chunk))
           message.on('end', () => controller.close())
           message.on('error', error => controller.error(error))
+          signal.addEventListener('abort', () => message.destroy(signal.reason))
         },
         cancel() {
           message.destroy()
diff --git a/lib/intercept.js b/lib/intercept.js
index bf7d14a..7c84f8e 100644
--- a/lib/intercept.js
+++ b/lib/intercept.js
@@ -399,7 +399,7 @@ function activate() {
           globalEmitter.emit('no match', nockRequest)
         } else {
           nockRequest.on('response', nockResponse => {
-            const response = createResponse(nockResponse)
+            const response = createResponse(nockResponse, mswRequest.signal)
             controller.respondWith(response)
           })
 


Test patch:
diff --git a/tests/test_fetch.js b/tests/test_fetch.js
index da6f2ed..3484b1f 100644
--- a/tests/test_fetch.js
+++ b/tests/test_fetch.js
@@ -119,6 +119,19 @@ describe('Native Fetch', () => {
     scope.done()
   })
 
+  it('should abort a request with a timeout signal', async () => {
+    const scope = nock('http://test.com').get('/').delayBody(100).reply(200)
+
+    const response = await fetch('http://test.com', {
+      signal: AbortSignal.timeout(50),
+    })
+    await assertRejects(
+      response.text(),
+      'TimeoutError: The operation was aborted due to timeout',
+    )
+    scope.done()
+  })
+
   // https://github.com/nock/nock/issues/2768
   it('should not mess the Headers object', async () => {
     nock('https://api.test.com', {