Skip to content

OpenTelemetry ESM instrumentation crashes: load hook returns null source for builtin format #669

Description

@colinhacks

Running an OpenTelemetry-instrumented ESM app under Nub crashes at startup. Plain Node with the same loader works.

Repro

npm init -y && npm pkg set type=module
npm i ms @opentelemetry/instrumentation
printf 'import ms from "ms";\nconsole.log("APP_OK");\n' > app.mjs
L=node_modules/@opentelemetry/instrumentation/hook.mjs

node --experimental-loader=$L app.mjs                 # exit 0, APP_OK
nub app.mjs                                           # exit 0, APP_OK
NODE_OPTIONS="--experimental-loader=$L" nub app.mjs   # exit 1
TypeError [ERR_INVALID_RETURN_PROPERTY_VALUE]: Expected a string, an ArrayBuffer, or a TypedArray to be returned for the "source" from the "load" hook but got null.
    at validateSourceStrict (node:internal/modules/customization_hooks:293:11)
    at validateLoadSloppy (node:internal/modules/customization_hooks:278:3)
    at #loadSync (node:internal/modules/esm/loader:832:16)

Nub 0.6.0, Node 26.5.0, macOS arm64.

Cause

The recovery path at runtime/preload-common.cjs:649-672 handles the loadSync stub Node throws when a user async module.register loader is present — which is what OpenTelemetry's ESM hook is. Its builtin branch returns:

return { format: "builtin", source: null, shortCircuit: true };

Node's validateSourceStrict accepts a null source only when format === "commonjs". Line 541 does that legally; the builtin branch does not, so the recovery path itself throws.

Ruled out

  • The version band in nodeHookComposeBroken() (22.15.0–24.11.0) leaves sync hooks installed on Node 26 rather than downgrading. Not the cause: a plain-Node sync registerHooks pass-through composed with the same OpenTelemetry async loader on Node 26.5.0 exits 0.
  • Setting __NUB_FORCE_ASYNC_TIER=1 is not a workaround, and cannot be tested that way — Nub strips the variable before the child sees it, so the child reads undefined. It is launcher-set only, via force_async_tier_env in spawn.rs. Crash traces with and without it are byte-identical.

Impact

Any ESM project using OpenTelemetry auto-instrumentation. The ESM hook is the only supported way to instrument ESM imports — the --require/--import register entrypoint alone registers no ESM loader — so this is the standard path, not an exotic configuration. No known workaround.

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