Skip to content

test: keep the suite runnable on Windows under Node 24+ - #361

Closed
nigrosimone wants to merge 1 commit into
dimdenGD:mainfrom
nigrosimone:fix-windows-tests
Closed

test: keep the suite runnable on Windows under Node 24+#361
nigrosimone wants to merge 1 commit into
dimdenGD:mainfrom
nigrosimone:fix-windows-tests

Conversation

@nigrosimone

Copy link
Copy Markdown
Contributor

Running npm test on Windows with Node 24 or later fails every single test, whatever the code does.

Why

Each test ends with process.exit() right after fetching. On Windows, Node 24 and later trip a libuv assertion when that happens while undici still holds keep-alive sockets:

Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94

The process dies with exit code 3221226505, and since the harness runs tests through exec(), that rejects and the test is reported as failed even though it did its work correctly. The crash also drops whatever stdout was still buffered, so the captured output is unreliable on top of the exit code, and comparisons against Express can silently be made against truncated text.

It is not this project: it reproduces with plain http and no Express or uWS at all.

const http = require("http");
const s = http.createServer((req, res) => res.end("ok"));
s.listen(13399, async () => {
    for (let i = 0; i < 3; i++) await fetch("http://localhost:13399/").then(r => r.text());
    process.exit(0);
});

Node 20.14 and 22.13 are fine, 24.0.2, 24.16, 25.0, 25.9 and 26.3 all crash on every run here. Upstream issue: nodejs/node#56645.

Fix

Preload a module on win32 that lets the loop settle before exiting for real. Linux and macOS, and therefore CI, do not load it and are untouched.

I measured the delay rather than guessing: 10ms still crashed 8 times out of 8, 50ms once out of 8, 100ms zero out of 8, so 100ms it is.

The tests that call process.exit() do it as their last statement, except events/event-finished.js, which calls it inside a finish handler where nothing follows either, so delaying it does not let anything extra run.

Result

Before: every test fails.

After, on Windows with Node 26.3: 300 tests, 298 pass, 2 fail. All categories pass except middlewares, which fails on multer.js and takes the category with it.

That multer failure is a separate, pre-existing problem, not something this change causes: on main its 200kb multipart upload fails with ECONNRESET 6 times out of 6 on Windows, while the same upload against Express 4 succeeds every time. It passes in CI, so it looks Windows-specific. I could not pin it down: a 200kb upload consumed with req.on("data"), with req.pipe(), or through express.raw() all work, with and without content-length, and so does the same FormData request when consumed by hand rather than by multer. Worth its own issue.

Every test ends with process.exit() right after fetching, which on
Windows with Node 24 and later trips a libuv assertion in undici's
socket teardown (nodejs/node#56645) and kills the process with exit code
3221226505. The harness runs each test through exec(), so that exit code
rejects and every test reports as failed. The crash also drops whatever
stdout was still buffered, so the captured output is unreliable even
when a test did its work correctly.

Preload a small module on win32 that lets the loop settle before exiting
for real. Linux and macOS, and therefore CI, are untouched.
@nigrosimone

Copy link
Copy Markdown
Contributor Author

I tracked down the multer failure I mentioned above, and I was wrong to call it Windows-specific. It is a timing problem, fixed in #362.

The test builds its 200kb file as new Array(209715).fill(0), which File reads as 209715 separate blob parts rather than bytes. Encoding that as multipart takes about 19 seconds here, and uWS resets a request body that arrives slower than 16kb/s after 10 idle seconds, so the socket dies mid-upload. Nothing to do with multer, multipart or the platform: a body deliberately stalled for 12 seconds is reset the same way, while Express 4 waits for it.

It reaches CI green only because the runner serialises those parts faster than the timeout. A slower runner would fail it there too.

With #362 applied, this branch gives 300 tests, 300 pass on Windows with Node 26.3.

@nigrosimone

Copy link
Copy Markdown
Contributor Author

Correcting the number I gave in my previous comment: with #362 applied this branch gives 300 tests, 298 pass on Windows with Node 26.3, not 300. I had deduced the total instead of measuring it, and there was a second failure hiding behind the multer one.

middlewares is fully green now (58/58). What is left is app, 17 of 19, failing on must support cluster, and that one is a genuine difference rather than a timing artifact:

- expected (Express):        { error: 'bind EADDRINUSE null:3000' }
+ actual (ultimate-express): { error: 'listen EADDRINUSE: address already in use :::3000' }

application.js builds that message itself:

let err = new Error('listen EADDRINUSE: address already in use :::' + port);

which is the wording Node uses on Linux. On Windows Node reports the failure against bind instead, so the strings differ and the comparison fails. That is also why it passes on CI.

I left it alone since it is a cosmetic difference in an error string, and making it platform-accurate means guessing at libuv wording per platform. Happy to send a patch if you would rather have it match.

@nigrosimone nigrosimone closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant