Repository: nodejs/undici (JavaScript)

Issue:
TypeError: Function.prototype.apply was called on null, which is a object and not a function
## Bug Description

Looks like the same bug as in #1327, but with `stream` and not `request`

## Reproducible By

```js
const {MockAgent, stream} = require('undici');
const {Writable} = require('stream');

async function main() {
    const mockAgent = new MockAgent();

    mockAgent.disableNetConnect();
    mockAgent
        .get('http://localhost')
        .intercept({
            path: '/'
        })
        .reply(200, 'ok')
        .delay(100);

    const parts = [];
    const ac = new AbortController();

    setTimeout(() => ac.abort('nevermind'), 50);

    try {
        await stream('http://localhost/', {
            opaque: {parts},
            signal: ac.signal,
            dispatcher: mockAgent
        }, ({opaque: {parts}}) => {
            return new Writable({
                write(chunk, _encoding, callback) {
                    parts.push(chunk);
                    callback();
                }
            });
        });

        console.log(Buffer.concat(parts).toString('utf-8'));
    } catch (error) {
        console.error('aborted', error);
    }
}

main().catch((e) => {
    console.error('fatal', e);
    process.exit(3);
});
```

## Expected Behavior

Should only log `RequestAbortedError` and exit with 0

## Logs & Screenshots

```
$ node repro.js
aborted RequestAbortedError [AbortError]: Request aborted
    at abort (project/node_modules/undici/lib/api/abort-signal.js:11:18)
    at AbortSignal.self.<computed> (project/node_modules/undici/lib/api/abort-signal.js:30:5)
    at AbortSignal.[nodejs.internal.kHybridDispatch] (node:internal/event_target:736:20)
    at AbortSignal.dispatchEvent (node:internal/event_target:678:26)
    at abortSignal (node:internal/abort_controller:292:10)
    at AbortController.abort (node:internal/abort_controller:323:5)
    at Timeout._onTimeout (project/repro.js:19:25)
    at listOnTimeout (node:internal/timers:559:17)
    at processTimers (node:internal/timers:502:7) {
  code: 'UND_ERR_ABORTED'
}
node:async_hooks:203
        ReflectApply(fn, thisArg, args);
        ^

TypeError: Function.prototype.apply was called on null, which is a object and not a function
    at StreamHandler.runInAsyncScope (node:async_hooks:203:9)
    at StreamHandler.onHeaders (project/node_modules/undici/lib/api/api-stream.js:107:18)
    at handleReply (project/node_modules/undici/lib/mock/mock-utils.js:278:13)
    at Timeout._onTimeout (project/node_modules/undici/lib/mock/mock-utils.js:247:7)
    at listOnTimeout (node:internal/timers:559:17)
    at processTimers (node:internal/timers:502:7)
$ echo $?
1
```

## Environment

node: v16.20.1
OS: Darwin Kernel Version 21.6.0

### Additional context

Probably should be fixed the same way, but in [`api-stream.js`](https://github.com/nodejs/undici/blob/main/lib/api/api-stream.js#L107)



Discussion:

Patch:
diff --git a/lib/api/api-stream.js b/lib/api/api-stream.js
index 3a8e71a5730..c571a6f79a7 100644
--- a/lib/api/api-stream.js
+++ b/lib/api/api-stream.js
@@ -104,6 +104,10 @@ class StreamHandler extends AsyncResource {
         { callback, body: res, contentType, statusCode, statusMessage, headers }
       )
     } else {
+      if (factory === null) {
+        return
+      }
+
       res = this.runInAsyncScope(factory, null, {
         statusCode,
         headers,
@@ -152,7 +156,7 @@ class StreamHandler extends AsyncResource {
   onData (chunk) {
     const { res } = this
 
-    return res.write(chunk)
+    return res ? res.write(chunk) : true
   }
 
   onComplete (trailers) {
@@ -160,6 +164,10 @@ class StreamHandler extends AsyncResource {
 
     removeSignal(this)
 
+    if (!res) {
+      return
+    }
+
     this.trailers = util.parseHeaders(trailers)
 
     res.end()


Test patch:
diff --git a/test/issue-2349.js b/test/issue-2349.js
new file mode 100644
index 00000000000..a82bb74a261
--- /dev/null
+++ b/test/issue-2349.js
@@ -0,0 +1,53 @@
+'use strict'
+
+const { test, skip } = require('tap')
+const { nodeMajor } = require('../lib/core/util')
+const { Writable } = require('stream')
+const { MockAgent, errors, stream } = require('..')
+
+if (nodeMajor < 16) {
+  skip('only for node 16')
+  process.exit(0)
+}
+
+test('stream() does not fail after request has been aborted', async (t) => {
+  t.plan(1)
+
+  const mockAgent = new MockAgent()
+
+  mockAgent.disableNetConnect()
+  mockAgent
+    .get('http://localhost:3333')
+    .intercept({
+      path: '/'
+    })
+    .reply(200, 'ok')
+    .delay(10)
+
+  const parts = []
+  const ac = new AbortController()
+
+  setTimeout(() => ac.abort('nevermind'), 5)
+
+  try {
+    await stream(
+      'http://localhost:3333/',
+      {
+        opaque: { parts },
+        signal: ac.signal,
+        dispatcher: mockAgent
+      },
+      ({ opaque: { parts } }) => {
+        return new Writable({
+          write (chunk, _encoding, callback) {
+            parts.push(chunk)
+            callback()
+          }
+        })
+      }
+    )
+  } catch (error) {
+    console.log(error)
+    t.equal(error instanceof errors.RequestAbortedError, true)
+  }
+})