-
-
Notifications
You must be signed in to change notification settings - Fork 731
Description
Bug Description
Calling abortController.abort() within the callback of undici.stream throws this error:
TypeError: Cannot read properties of null (reading 'readable')
at Stream.<anonymous> (/project/workspace/node_modules/undici/lib/api/api-stream.js:120:23)
Reproducible By
Runnable reproduction at https://codesandbox.io/p/devbox/dy3jrd
I created the following function to stream a JSON response from a remote server and parse out just the JSON object path I care about, and abort the download as soon as I found what I want:
import { stream as undiciStream } from 'undici'
import { parse as jsonParse } from 'JSONStream' // https://github.com/dominictarr/JSONStream
/**
* Fetch JSON from the given URL and parse the response as a stream, returning the specified path from the object.
* @throws {Error} If the request fails or if the JSON path is not found.
* @param {string} url
* @param {string} jsonPath
*/
export function fetchAndFindPath(url, jsonPath) {
return new Promise((resolve, reject) => {
const abortController = new AbortController()
undiciStream(url, {
signal: abortController.signal,
method: 'GET',
}, ({ statusCode }) => {
if (statusCode >= 400) {
const errorMessage = `Undici stream failed: ${statusCode} for GET of ${url}`
console.error(errorMessage)
throw new Error(errorMessage)
}
let found = false
return jsonParse(jsonPath)
.on('error', error => {
console.error('Error parsing JSON from %s at path "%s":', url, jsonPath, error)
reject(error)
})
.on('data', data => {
found = true
resolve(data)
abortController.abort() // Comment out this line to make the error disappear
})
.on('end', () => {
if (!found) {
reject(new Error(`No data found at path "${jsonPath}" in response from ${url}`))
}
})
}).then(resolve).catch(reject)
})
}Using this via a call like fetchAndFindPath('https://nodejs.org/dist/index.json', '0.version') throws TypeError: Cannot read properties of null (reading 'readable').
Commenting out abortController.abort() eliminates the error, though presumably this means that the download continues in the background when I don’t need it to.
Alternatively, replacing AbortController with EventEmitter also eliminates the error. See the reproduction for this version.
Expected Behavior
Using abortController.abort() to stop a stream should function identically to using eventEmitter.emit('abort') to stop it.
Environment
Node.js 24.1.0, Undici 7.10.0
Additional context
For anyone else stumbling across this, the alternative version below that uses EventEmitter doesn’t have this issue:
import EventEmitter from 'node:events'
import { stream as undiciStream } from 'undici'
import { parse as jsonParse } from 'JSONStream' // https://github.com/dominictarr/JSONStream
/**
* Fetch JSON from the given URL and parse the response as a stream, returning the specified path from the object.
* @throws {Error} If the request fails or if the JSON path is not found.
* @param {string} url
* @param {string} jsonPath
*/
export function fetchAndFindPath(url, jsonPath) {
return new Promise((resolve, reject) => {
const eventEmitter = new EventEmitter()
undiciStream(url, {
signal: eventEmitter,
method: 'GET',
}, ({ statusCode }) => {
if (statusCode >= 400) {
const errorMessage = `Undici stream failed: ${statusCode} for GET of ${url}`
console.error(errorMessage)
throw new Error(errorMessage)
}
let found = false
return jsonParse(jsonPath)
.on('error', error => {
console.error('Error parsing JSON from %s at path "%s":', url, jsonPath, error)
reject(error)
})
.on('data', data => {
found = true
resolve(data)
eventEmitter.emit('abort')
})
.on('end', () => {
if (!found) {
reject(new Error(`No data found at path "${jsonPath}" in response from ${url}`))
}
})
}).then(resolve).catch(reject)
})
}As an aside, this “stream some JSON, pull out only what you want, and stop downloading as soon as you have it” function might be a good example to add to the Undici docs? It feels like a potentially generic use case, and a good example for undici.stream. If any Undici experts want to code review it to tell me how it could be optimized (should I be putting anything on opaque, for instance) then I would be happy to submit a PR to add it to the docs. (Though we should probably add an example that uses AbortController, which means fixing this bug first 😄)