You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Undici.Request and AbortController doesn't work well #3353
node:events:498
throw er; // Unhandled 'error' event
^
DOMException [AbortError]: This operation was aborted
at new DOMException (node:internal/per_context/domexception:53:5)
at AbortController.abort (node:internal/abort_controller:395:18)
at Timeout._onTimeout (T:\xx\other scripts\undici-debug.js:7:31)
at listOnTimeout (node:internal/timers:573:17)
at process.processTimers (node:internal/timers:514:7)
Emitted 'error' event on BodyReadable instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
Node.js v22.3.0
Environment
Ubuntu 24.04 & Windows 11
Node v22.3.0
Undici 6.19.1
I am having the same problem. The bug was introduced in 6.16.0. I think the problem is in this #3209. In case of onComplete we don't remove the listener from the signal
Hi team, we got bitten by this issue in production like others in this thread.
I think that while Readable and undici are each working as intended, the combination of the two leads to an API that is unnecessarily hostile for a user.
In our case, the issue manifested because we're holding onto a reference to clientRes to pipe into a subsequent bit of work. However, after obtaining a reference to clientRes and before calling pipeline(clientRes.body, nextConsumer, { signal }), we call signal.throwIfAborted().
This leads to a whole-program crash despite the whole sequence above being orchestrated in an async function, attempting idiomatic use of AbortSignal.
The fix, in our case is the following, which I believe is indicative of the dangerous nature of this interaction:
asyncfunctioncriticalPath(){clientRes=awaitperformUndiciRequest(signal);// This is the fix.clientRes.body.on('error',()=>{// Ignore the error so that we don't get uncaught exceptions and crash.});// ... do some async worksignal.throwIfAborted();// ... consume the bodyawaitpipeline([clientRes.body,targetStream],{ signal });}
@kyrylkov can you elaborate on how you solved this via code snippet?
So we use undici fetch that returns WebStream readable. We then convert that readable into Node.js readable. Then we async iterate over it. Turns out that after we consume the whole readable, the stream wouldn't close for some reason. We've started explicitly destroying stream after consuming them and the problem went away. Still not sure why streams were not destroyed implicitly, because our understanding is that they should have.
how do you safely do cleanup and destroy the stream after? otherwise you will encounter 100% cpu usage
@titanism my understanding is that the problematic behaviour with CPU usage is limited to fetch and Web streams. In our case, we're using undici as a lower-overhead http/1.1 client for some high-volume, low-latency reverse proxying.
Is there evidence that import('node:stream').Readable also suffers from this footgun?
I suppose we should use node-fetch or an alternative instead, e.g. XHR due to the CPU issues.
@ronag How do you properly destroy a fetch? See that Reddit link above; we have the exact same issue that @kyrylkov had, which is using PM2 and after ~20-30 mins time, undici fetch calls seem to cause event loop to build-up and queue, due to Readable. We used v8-profiler-next and cpupro, but it's very limited in insight:
I think the surprising thing here is that this is a hybrid promise / stream API. Both the initial promise for getting the Response object AND the stream are governed by the signal. I suppose that I was only expecting the initial Promise for the headers to be controlled by the signal. I was already controlling the consumption of the body stream using the signal via pipeline(). I expected any possible failure modes to result in promise rejections instead of this subtle interaction with node's emitter-based legacy.
I think the surprising thing here is that this is a hybrid promise / stream API. Both the initial promise for getting the Response object AND the stream are governed by the signal. I suppose that I was only expecting the initial Promise for the headers to be controlled by the signal. I was already controlling the consumption of the body stream using the signal via pipeline(). I expected any possible failure modes to result in promise rejections instead of this subtle interaction with node's emitter-based legacy.
@mcollina wdyt? I think it's a reasonable opinion but not sure what to do with it. Would break everyone to change it now.
Activity
Gigioliva commentedon Jun 21, 2024
I am having the same problem. The bug was introduced in
6.16.0. I think the problem is in this #3209. In case of onComplete we don't remove the listener from the signalGigioliva commentedon Jun 21, 2024
Adding this in
api-request.jsfixes the issuemcollina commentedon Jun 21, 2024
@Gigioliva Would you like to send a Pull Request to address this issue? Remember to add unit tests.
Gigioliva commentedon Jun 21, 2024
👍🏼 I will do
Gigioliva commentedon Jun 21, 2024
@mcollina #3354
ronag commentedon Jun 22, 2024
The problem is that you are not cleaning up / destroying / consuming the body of returned response object which keep the signal handler referenced.
Gigioliva commentedon Jun 22, 2024
@ronag In my case I was consuming the response 🤔
await result.body.text()ronag commentedon Jun 22, 2024
Not between iterations of the while loop
Gigioliva commentedon Jun 22, 2024
Yes. within the loop. My code is something like:
ronag commentedon Jun 22, 2024
Do you have a repro then? Because your other repro was incorrect. Without a repro we cannot help.
Gigioliva commentedon Jun 22, 2024
Yup. I can reproduce it in my tool tests. I will try to extract the test and share with you
fawazahmed0 commentedon Jun 22, 2024
Here is a repro:
Update: Removed unnecessary
fetchfrom repro21 remaining items
fix: attempt to fix uncaught exception per <nodejs/undici#3353>
uriva commentedon Nov 3, 2024
@titanism I'm still seeing this issue on nodejs 23.1.0
the code in question is https://github.com/webtorrent/bittorrent-tracker/blob/master/lib/client/http-tracker.js#L147
I'm getting the
DOMExceptionfrom the line where theAbortControlleris created, that is outside thetry/catchAny idea?
Thanks
(original issue: webtorrent/webtorrent#2874)
ggoodman commentedon Nov 12, 2024
Hi team, we got bitten by this issue in production like others in this thread.
I think that while
Readableandundiciare each working as intended, the combination of the two leads to an API that is unnecessarily hostile for a user.In our case, the issue manifested because we're holding onto a reference to
clientResto pipe into a subsequent bit of work. However, after obtaining a reference toclientResand before callingpipeline(clientRes.body, nextConsumer, { signal }), we callsignal.throwIfAborted().This leads to a whole-program crash despite the whole sequence above being orchestrated in an
async function, attempting idiomatic use ofAbortSignal.The fix, in our case is the following, which I believe is indicative of the dangerous nature of this interaction:
titanism commentedon Nov 12, 2024
@ggoodman how do you safely do cleanup and destroy the stream after? otherwise you will encounter 100% cpu usage, e.g. https://www.reddit.com/r/node/comments/vcxx4n/comment/ideo7s4/
titanism commentedon Nov 12, 2024
@kyrylkov can you elaborate on how you solved this via code snippet?
ggoodman commentedon Nov 12, 2024
@titanism my understanding is that the problematic behaviour with CPU usage is limited to
fetchand Web streams. In our case, we're usingundicias a lower-overhead http/1.1 client for some high-volume, low-latency reverse proxying.Is there evidence that
import('node:stream').Readablealso suffers from this footgun?ronag commentedon Nov 12, 2024
I don't see a problem here. You are mostly mis-using the api... The
signal.throwIfAborted()is not necessary.titanism commentedon Nov 12, 2024
I suppose we should use
node-fetchor an alternative instead, e.g. XHR due to the CPU issues.@ronag How do you properly destroy a fetch? See that Reddit link above; we have the exact same issue that @kyrylkov had, which is using PM2 and after ~20-30 mins time,
undicifetch calls seem to cause event loop to build-up and queue, due to Readable. We usedv8-profiler-nextandcpupro, but it's very limited in insight:titanism commentedon Nov 12, 2024
Anonymous function #994 is this:
ronag commentedon Nov 12, 2024
@titanism That does not seem relateed to this issue, please create a new one
titanism commentedon Nov 12, 2024
Usage here https://github.com/forwardemail/nodejs-dns-over-https-tangerine/blob/4403cbabcb8b19ab2b118c00a461c6c7e64407dc/index.js#L1119-L1159
ggoodman commentedon Nov 12, 2024
@ronag I think I see where you're coming from now. This appears to be the precedent for all Node.js Streams produced with a supplied signal (https://github.com/nodejs/node/blob/f17b9a4c7838f37022255f4dcee09a083686018e/lib/internal/streams/add-abort-signal.js#L41-L60). At least, that's the precedent for synchronous APIs returning streams.
I think the surprising thing here is that this is a hybrid promise / stream API. Both the initial promise for getting the
Responseobject AND the stream are governed by thesignal. I suppose that I was only expecting the initial Promise for the headers to be controlled by the signal. I was already controlling the consumption of the body stream using thesignalviapipeline(). I expected any possible failure modes to result in promise rejections instead of this subtle interaction with node's emitter-based legacy.ronag commentedon Nov 13, 2024
@mcollina wdyt? I think it's a reasonable opinion but not sure what to do with it. Would break everyone to change it now.
Bugfix - when using AbortController, errors on resulting stream must …
fix: attempt to fix uncaught exception per <nodejs/undici#3353>