nodejs/node · error · InvalidReturnValueError

UND_ERR_INVALID_RETURN_VALUE

UND_ERR_INVALID_RETURN_VALUE

Error message

expected Readable

What it means

Thrown by undici's PipelineHandler (code UND_ERR_INVALID_RETURN_VALUE) when the user-supplied handler returns a value that is null/undefined or lacks an 'on' method — i.e. not a Readable stream. pipeline() pumps the response body into the stream returned by the handler, so the return value MUST be a Readable (or have Readable-like .on('data'|'error'|'end')). This is a contract violation by the handler implementation, not a network error.

Source

Thrown at deps/undici/src/lib/api/api-pipeline.js:203

      this.handler = null
      const rawHeaders = controller?.rawHeaders
      const responseHeaders = this.responseHeaders === 'raw'
        ? util.parseRawHeaders(rawHeaders)
        : headers
      body = this.runInAsyncScope(handler, null, {
        statusCode,
        headers: responseHeaders,
        opaque,
        body: this.res,
        context
      })
    } catch (err) {
      this.res.on('error', noop)
      throw err
    }

    if (!body || typeof body.on !== 'function') {
      throw new InvalidReturnValueError('expected Readable')
    }

    body
      .on('data', (chunk) => {
        const { ret, body } = this

        if (!ret.push(chunk) && body.pause) {
          body.pause()
        }
      })
      .on('error', (err) => {
        const { ret } = this

        util.destroy(ret, err)
      })
      .on('end', () => {
        const { ret } = this

View on GitHub (pinned to 1b2de5e052)

Solutions

  1. Ensure the handler returns a Readable stream — typically create one with new PassThrough()/Readable.from() and return it.
  2. Do not return a Promise; do the work synchronously and return the stream, or pipe inside the handler.
  3. If transforming, read from the provided res.body and push into a Readable you create and return.
  4. Verify the returned object has .on('data')/.on('error')/.on('end').

Example fix

// before
undici.pipeline(url, {}, ({ body }) => {
  body.on('data', () => {})   // returns undefined -> 'expected Readable'
})
// after
const { PassThrough } = require('node:stream')
undici.pipeline(url, {}, ({ body }) => {
  const pass = new PassThrough()
  body.pipe(pass)
  return pass
})
Defensive patterns

Strategy: type-guard

Validate before calling

function ensureReadable(ret) {
  if (!ret || typeof ret.on !== 'function') throw new TypeError('pipeline handler must return a Readable stream')
  return ret
}

Type guard

function isReadableLike(v: unknown): v is NodeJS.ReadableStream {
  return !!v && typeof (v as any).on === 'function'
}

Try / catch

// pipeline throws synchronously inside the handler scope; wrap handler body:
undici.pipeline(url, opts, (res) => {
  try {
    const ret = transform(res)
    if (!ret || typeof ret.on !== 'function') throw new TypeError('handler must return a Readable')
    return ret
  } catch (err) { console.error(err); throw err }
})

Prevention

When it happens

Trigger: pipeline() handler that returns undefined (forgot return), a string, a Promise, an object literal, or a Writable-only stream without .on. Any of these causes onResponseStart to throw 'expected Readable'.

Common situations: Handler returns nothing after `await`; handler returns the response object itself instead of a transformed stream; handler returns a Promise of a stream (pipeline does not await); passing a sink/Writable where a Readable is required.

Related errors


AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13). Data as JSON: /api/errors/bc49ff6d3dcaa19a. Report an issue: GitHub.