dánwòdánwò

Real stacks

The Node preload

How danwo routes a Node app's calls through its proxy without code changes, and what to do when a start script gets in the way.

Your app calls api.stripe.com and gets danwo's emulator, without a line of code changed. For a Node app, a small preload does that: a module Node loads before your app starts, which sends every outgoing HTTP call through danwo's proxy and trusts its certificate.

How it gets in

danwo adds the preload to NODE_OPTIONS in each app's environment, together with the proxy's address and the run's CA certificate. Calls made with Node's own HTTP clients, axios and most SDKs then go through the proxy. That includes clients that build their own agent:

ClientRouted
fetch, http, https✓
Stripe's client with its own https.Agent✓
axios with a custom agent✓
fetch with an undici Agent✓
a server-side WebSocket clientonly with a proxy agent (why)

Your app's Node version

danwo itself needs Node 24. Your app can run on an older Node: a package pinned by volta, mise or asdf, a script that calls a specific node, or a Docker image. The preload routes Node 20 and later. Newer Nodes do part of the work themselves, and the preload does the rest:

Node running the appWho routes fetchWho routes http, https and their agents
24.5 and later, 22.21 and later on 22NodeNode, with the preload's help for SDK agents
24.0 to 24.4Nodethe preload
20, 22.0 to 22.20, 23the preloadthe preload
18 and oldernot supportednot supported

Where Node can't route a call, the preload opens the tunnel through the proxy itself and follows NO_PROXY as Node does.

The check at start

Before your app's code runs, the preload calls a probe host that only danwo's proxy answers, with fetch, an https.Agent and an http.Agent. A process counts as routed only when all three reach the proxy. If one misses, the process says so on stderr, naming its pid, its Node version and what to do:

stderr · a process that misses the proxy
danwo: process 41872 runs Node v22.18.0, and its calls do not go through danwo's proxy
(fetch (ENOTFOUND) missed it): they would reach the real providers. …

What danwo does next depends on the run:

Run settingsA process that misses the proxy
unmatched: "block" or secrets: "fail"stops at start, and danwo refuses the run with its name
anything elsekeeps running; danwo warns and adds it to the run notes

The usual cause is a NO_PROXY that names every host (*) or the providers' hosts.

When a start script sets NODE_OPTIONS

Many start scripts set NODE_OPTIONS themselves:

package.json · a start script
{ "scripts": { "start": "NODE_OPTIONS=--max-old-space-size=8192 nest start" } }

That replaces danwo's value, and the preload is lost. danwo handles it for you: it puts a small node shim first on each app's PATH, which loads the preload whatever NODE_OPTIONS says. Your script keeps its own flags too.

A NODE_OPTIONS you set in an app's env in the config is joined with danwo's, not replaced:

danwo.config.ts · joined flags
{ name: "api", command: "pnpm start", url: "http://localhost:4000",
  env: { NODE_OPTIONS: "--max-old-space-size=4096" } }

When a process still escapes

Each process the preload is in leaves a heartbeat. After the apps start, danwo warns about any Node process of an app that runs without it, and says how to fix it. The usual causes:

CauseFix
the script runs Node by an absolute path (/usr/bin/node)call node from PATH
the app spawns Node with a clean environmentpass process.env to the child
the shim is off (nodeShim: false)turn it back on, or set NODE_OPTIONS in env instead
the app runs Node 18 or olderrun it on Node 20 or later

A process without the preload calls the real hosts, and unmatched: "block" can't stop it: its calls never reach the proxy.

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI