Skip to content

stripIndexParam&stripRoutesParam pattern is triggering a bug in undici causing AbortSignal to be skipped #14817

@Menduist

Description

@Menduist

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.1

Used 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions