-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
Description
Reproduction
// Run: node --expose-gc repro.mjs
function wrapRequest(request) {
// Similar to stripIndexParam or stripRoutesParam
let url = new URL(request.url);
let init = { method: request.method, body: request.body, headers: request.headers, signal: request.signal };
if (init.body) init.duplex = "half";
return new Request(url.href, init);
}
async function test(label, buildRequest) {
const ac = new AbortController();
const original = new Request("http://localhost/?index&_routes=x", { signal: ac.signal });
const final = buildRequest(original);
// Force GC to collect any intermediate Request objects
global.gc();
await new Promise(r => setTimeout(r, 100));
global.gc();
await new Promise(r => setTimeout(r, 100));
let aborted = false;
final.signal.addEventListener("abort", () => { aborted = true; });
ac.abort();
await new Promise(r => setTimeout(r, 50));
console.log(`${label}: ${aborted ? "PASS (signal propagated)" : "FAIL (signal lost)"}`);
}
await test("Single wrap ", (req) => wrapRequest(req));
await test("Chained wrap (bug)", (req) => wrapRequest(wrapRequest(req)));System Info
System:
OS: Linux 6.18 Arch Linux
CPU: (16) x64 AMD Ryzen 9 7940HS w/ Radeon 780M Graphics
Memory: 17.03 GB / 30.56 GB
Container: Yes
Shell: 5.3.9 - /bin/bash
Binaries:
Node: 22.16.0
Yarn: 1.22.22 - /usr/bin/yarn
npm: 10.9.2
pnpm: 10.29.1 - /usr/bin/pnpm
bun: 1.3.4 - /usr/bin/bun
Deno: 2.6.5 - /usr/bin/deno
Browsers:
Firefox: 147.0.3
Firefox Developer Edition: 147.0.3
npmPackages:
@react-router/dev: ^7.13.0 => 7.13.0
@react-router/fs-routes: ^7.13.0 => 7.13.0
@react-router/node: ^7.13.0 => 7.13.0
@react-router/serve: ^7.13.0 => 7.13.0
react-router: ^7.13.0 => 7.13.0
vite: ^7.3.1 => 7.3.1Used Package Manager
npm
Expected Behavior
Sorry about the repro not being inside react-router, that would be quite cumbersome
But basically, everytime the Request is wrapped (like in packages/react-router/lib/server-runtime/data.ts stripRoutesParam & stripIndexParam), this might trigger this bug in undici: nodejs/undici#4068
Where the "intermediary" request AbortSignal can be picked up by the GC, and the chain of AbortSignal to the original request is then lost.
In our case, that caused a leak of SSE connections that are never closed.
Actual Behavior
AbortSignal working correctly.
undici had this bug for a while and doesn't seem eager to fix it, so it would be great to workaround it in react-router. Though I don't see a good way to do so, except to stop rewriting the URL like the comment above is suggesting (stop stripping these in V2.)
EDIT: Just found out that the undici issue was originally flagged thanks to remix: remix-run/remix#10861, but it has not been fixed