Skip to content

TypeError when using AbortController on an Undici stream #4248

@GeoffreyBooth

Description

@GeoffreyBooth

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 😄)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions