From 4f3b4ea9ac4c5a2c51c694de58e482ad379d2440 Mon Sep 17 00:00:00 2001 From: Emilio Astarita Date: Wed, 22 Jul 2026 10:53:18 -0300 Subject: [PATCH] Return Buffer from readFileWithWorker so wrapped res.end() accepts static files The file worker (used for files < 768KB, enabled by default on multi-core machines where threads defaults to 1) transfers file contents as an ArrayBuffer. ultimate-express's own res.end() accepts an ArrayBuffer, so a bare app serves these files fine. But when res.end has been wrapped per-request by middleware that inspects the chunk -- express-session is the canonical case, it calls Buffer.byteLength(chunk) to set Content-Length -- the ArrayBuffer is rejected with ERR_INVALID_ARG_TYPE. The rejection lands in sendFile's .catch, which calls the static middleware's fallthrough next(), and the request ends up at the outer 404 handler. Net effect: every static file under 768KB mounted after express-session returns 404 on multi-core boxes with default settings, even though the file exists and is readable. Wrap the worker result in a Buffer (zero-copy) at the resolve boundary so readFileWithWorker returns the same type as fs.readFile, which every express-compatible end() wrapper accepts. --- src/application.js | 5 ++- .../middlewares/express-static-session.js | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/tests/middlewares/express-static-session.js diff --git a/src/application.js b/src/application.js index 5c659bd3..4172956a 100644 --- a/src/application.js +++ b/src/application.js @@ -40,7 +40,10 @@ class FSWorker { if(message.err) { workerTasks[message.key].reject(new Error(message.err)); } else { - workerTasks[message.key].resolve(message.data); + // worker transfers file contents as an ArrayBuffer; wrap it in a Buffer (zero-copy) so + // consumers get the same type as fs.readFile. A bare ArrayBuffer is rejected by wrapped + // res.end() implementations (e.g. express-session calls Buffer.byteLength on the chunk). + workerTasks[message.key].resolve(message.data instanceof ArrayBuffer ? Buffer.from(message.data) : message.data); } delete workerTasks[message.key]; }); diff --git a/tests/tests/middlewares/express-static-session.js b/tests/tests/middlewares/express-static-session.js new file mode 100644 index 00000000..4fdba60f --- /dev/null +++ b/tests/tests/middlewares/express-static-session.js @@ -0,0 +1,39 @@ +// must serve static files under 768KB when res.end is wrapped by express-session + +const express = require("express"); +const session = require("express-session"); + +// force the non-optimized dispatch path (the uWS route optimizer masks this bug), +// and route small files through the file worker (default on multi-core machines) +function makeApp(options, useSession) { + const app = options ? express(options) : express(); + app.set("case sensitive routing", false); + if(useSession) { + app.use(session({ secret: "x", resave: false, saveUninitialized: true })); + } + app.use("/static", express.static("tests/parts")); + app.use((req, res) => { + res.status(404).send("catchall"); + }); + return app; +} + +async function check(label, app, port, file) { + await new Promise(resolve => app.listen(port, resolve)); + await new Promise(resolve => setTimeout(resolve, 200)); // wait past the optimizer window + const res = await fetch(`http://localhost:${port}/static/${file}`); + const body = await res.text(); + console.log(label, res.status, body.length); +} + +(async () => { + // small file (< 768KB): worker path. With default threads on multi-core, this is the bug. + await check("default+session small", makeApp(undefined, true), 13334, "index.ejs"); + // control: no workers, so the pipe path is used + await check("threads0+session small", makeApp({ threads: 0 }, true), 13335, "index.ejs"); + // control: without session, an unwrapped end() accepts the worker result + await check("default nosession small", makeApp(undefined, false), 13336, "index.ejs"); + // control: file > 768KB is streamed, bypassing the worker + await check("default+session large", makeApp(undefined, true), 13337, "big.jpg"); + process.exit(0); +})();