Skip to content

Undici request() from Dispatcher base class only uses undocumented handler API, not new handler API #4780

@domenic

Description

@domenic

Bug Description

When attempting to write a custom dispatcher according to the documentation, it is natural to inherit from Dispatcher, or maybe DispatcherBase. Then one implements one's dispatch() method, and hopes that other methods like request() will work for free.

This is not the case, because request() only works if one uses the old, undocumented handlers API.

Reproducible By

"use strict";
const { Dispatcher } = require("undici");
const DispatcherBase = require("undici/lib/dispatcher/dispatcher-base");

class OldAPIDispatcher extends Dispatcher {
  dispatch(opts, handler) {
    console.log("Available methods:", Object.getOwnPropertyNames(Object.getPrototypeOf(handler)));

    handler.onConnect(() => {});
    handler.onHeaders(200, { "content-type": "text/plain" }, () => {}, "OK");
    handler.onData(Buffer.from("Hello, world!"));
    handler.onComplete({});
    return true;
  }
}

class NewAPIDispatcher extends Dispatcher {
  dispatch(opts, handler) {
    console.log("onResponseStart exists:", !!handler.onResponseStart);

    handler.onRequestStart?.(null, {});
    handler.onResponseStart?.(null, 200, { "content-type": "text/plain" }, "OK");
    handler.onResponseData?.(null, Buffer.from("Hello, world!"));
    handler.onResponseEnd?.(null, {});
    return true;
  }
}

class NewAPIDispatcherBase extends DispatcherBase {
  dispatch(opts, handler) {
    console.log("onResponseStart exists:", !!handler.onResponseStart);

    handler.onRequestStart?.(null, {});
    handler.onResponseStart?.(null, 200, { "content-type": "text/plain" }, "OK");
    handler.onResponseData?.(null, Buffer.from("Hello, world!"));
    handler.onResponseEnd?.(null, {});
    return true;
  }
}

async function test(name, dispatcher) {
  console.log(`\n${name}:`);
  const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out")), 500));
  try {
    const res = await Promise.race([
      dispatcher.request({ origin: "http://example.com", path: "/", method: "GET" }),
      timeout
    ]);
    console.log("  Success:", await res.body.text());
  } catch (e) {
    console.log(" ", e.message);
  }
}

(async () => {
  await test("Old API (Dispatcher)", new OldAPIDispatcher());
  await test("New API (Dispatcher)", new NewAPIDispatcher());
  await test("New API (DispatcherBase)", new NewAPIDispatcherBase());
})();

Expected Behavior

Available methods lists the new, documented handler API (onRequestStart, onRequestUpgrade, etc.)

At least the New API (DispatcherBase) case works, and ideally also the New API (Dispatcher) case.

Logs & Screenshots

Old API (Dispatcher):
Available methods: [
  'constructor',
  'onConnect',
  'onHeaders',
  'onData',
  'onComplete',
  'onError'
]
  Success: Hello, world!

New API (Dispatcher):
onResponseStart exists: false
  Timed out

New API (DispatcherBase):
onResponseStart exists: false
  Timed out

Environment

  • Node v25.3.0
  • undici 7.19.2

Additional context

Somewhat related to #4771.

In #4753 (comment) I was told not to rely on the old handler API existing, so I'm unsure how to proceed here.

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