-
Notifications
You must be signed in to change notification settings - Fork 530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Undici.Request and AbortController doesn't work well #3353
Comments
I am having the same problem. The bug was introduced in |
@Gigioliva Would you like to send a Pull Request to address this issue? Remember to add unit tests. |
👍🏼 I will do |
The problem is that you are not cleaning up / destroying / consuming the body of returned response object which keep the signal handler referenced. while (!response?.ok) {
await response?.body.dump()
response = await undici.request(link,{maxRedirections: 30, signal: controller?.signal})
} |
@ronag In my case I was consuming the response 🤔 |
Not between iterations of the while loop |
Yes. within the loop. My code is something like: class MyClass {
readonly #pool: Agent;
constructor() {
this.#pool = new Agent({ connections: MAX_CONCURRENCY });
}
async runFetch(urls: string[]): Promise<string[]> {
return await Promise.all(urls.map((url) => this.doGet(url)));
}
private async doGet(url: string) {
const result = await this.#pool.request({
origin: url,
path: "/foo",
method: "GET",
signal: AbortSignal.timeout(1_000),
});
const text = await result.body.text();
console.log(text);
return text;
}
} |
Do you have a repro then? Because your other repro was incorrect. Without a repro we cannot help. |
Yup. I can reproduce it in my tool tests. I will try to extract the test and share with you |
Here is a repro: const undici = require('undici')
async function testUndici(link, controller) {
let undiciResponse;
while (!(undiciResponse?.statusCode >= 200 && undiciResponse?.statusCode < 300))
undiciResponse = await undici.request(link, { maxRedirections: 30, signal: controller?.signal })
sleep(500).then(() => controller.abort())
}
async function begin() {
const controller = new AbortController()
while (true)
await testUndici("https://github.com/fawazahmed0/tiger/releases/download/v2/pipetocsv.7z", controller).catch(console.error)
}
begin()
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
} Update: Removed unnecessary |
You are not consuming the body of the undici.request response object between loop iterations. Why are you including fetch? Is the problem with request or fetch? |
@ronag looking better at the code, for a specific status code ( Note: adding a "useless" I get the error even if I don't consume the body with a 40X... why I need to consume the body after an error? |
I agree with @Gigioliva |
That's how it was designed and is clearly documented. We can discuss that in a separate issue if you wish. |
@ronag Could you point me to the documentation which says that using abort() will kill the whole node process. I can find one example which says that using abort() will throw Another repro (example copied from doc): import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const abortController = new AbortController()
client.request({
path: '/',
method: 'GET',
signal: abortController.signal
}).catch(error=>{
console.log('Hi! Error Caught')
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
})
setTimeout(()=>abortController.abort(),500) |
request returns a body: Stream which gets destroyed with an abort error and emits an error that you are not handling and therefore the process crashes with an unhandled exception. This is normal nodejs behavior. Feel free to open a PR if you think that needs to be explicitly documented by undici. client.request({
path: '/',
method: 'GET',
signal: abortController.signal
}).then(res=>{
console.log('Hi! Response received')
res.body.on('error', error=>{
console.log('Hi! Error Caught')
console.error(error) // prints a RequestAbortedError
client.close()
server.close()
})
}).catch(error=>{
console.log('Hi! Error Caught')
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
})
setTimeout(()=>abortController.abort(),500) |
Is this considered "correct"? const pool = new Agent()
try {
const result = await this.#pool.request({
origin: url.origin,
path: url.pathname,
method: "GET",
signal: AbortSignal.timeout(1000),
});
if(result.statusCode === 204){
// log and DON'T consume the body
} else {
// call await result.body.text()
}
// Manually close the body to avoid this bug
result.body.emit("close");
} catch(err) {
// Handle connection errors
}
|
No that is not correct. You should not be emitting events yourself. const pool = new Agent()
try {
const result = await this.#pool.request({
origin: url.origin,
path: url.pathname,
method: "GET",
signal: AbortSignal.timeout(1000),
});
if(result.statusCode === 204){
await result.body.dump()
} else {
await result.body.text()
}
} catch(err) {
// Handle connection errors
} |
Any update yet on a proper fix for this? |
There is nothing to fix. It works as designed. |
… body if not used (per <nodejs/undici#3353>)
This is quite odd, we are listening for the error but it is still throwing an uncaught exception. @ronag any idea why? |
Oh, we see the issue, we aren't calling |
No, that was not the case. This is definitely a breaking bug and should have been more carefully done with a major release. |
@ronag we're stumped and cannot figure out why we are getting uncaught DOMException errors thrown (causing our entire process to restart) here's the code <https://github.com/forwardemail/nodejs-dns-over-https-tangerine/blob/main/index.js |
Could it be that |
@ronag There is a call here which does nothing: Line 169 in 00dfd0b
Lines 286 to 290 in 00dfd0b
That function |
Nevermind, |
I believe we've fixed the issue per #3353. |
Bug Description
Sometimes Aborting Undici.Request causes whole program to stop, even though everything is wrapped around try catch.
Reproducible By
I don't have minimal reproducible example, as my code is larger in size.But Undici.Fetch doesn't have this issue.Here is minimal reproducible example:
Expected Behavior
Undici.Request and AbortController should work
Logs & Screenshots
Environment
Ubuntu 24.04 & Windows 11
Node v22.3.0
Undici 6.19.1
The text was updated successfully, but these errors were encountered: