From aaab009c48b31b067a1b45d1cc968e1648093d79 Mon Sep 17 00:00:00 2001 From: Justin Vaillancourt Date: Wed, 25 Sep 2024 22:26:13 -0700 Subject: [PATCH] fix: request signal not being aborted after request is garbage collected --- .changeset/stale-tips-live.md | 5 +++++ contributors.yml | 1 + packages/remix-server-runtime/data.ts | 32 +++++++++++++++++++-------- 3 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 .changeset/stale-tips-live.md diff --git a/.changeset/stale-tips-live.md b/.changeset/stale-tips-live.md new file mode 100644 index 00000000000..bf6b1dd1bc6 --- /dev/null +++ b/.changeset/stale-tips-live.md @@ -0,0 +1,5 @@ +--- +"@remix-run/server-runtime": patch +--- + +Fix request signal not aborting after request is garbage collected diff --git a/contributors.yml b/contributors.yml index ef460350f43..fe74dec8fb8 100644 --- a/contributors.yml +++ b/contributors.yml @@ -343,6 +343,7 @@ - jvnm-dev - jwaltz - jwnx +- jvaill - kalch - kamtugeza - kandros diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index afdda2ac145..6d6217e1fdc 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -43,9 +43,11 @@ export async function callRouteAction({ singleFetch: boolean; }) { let result = await action({ - request: singleFetch - ? stripRoutesParam(stripIndexParam(request)) - : stripDataParam(stripIndexParam(request)), + request: makeRequestSignalGcSafe( + singleFetch + ? stripRoutesParam(stripIndexParam(request)) + : stripDataParam(stripIndexParam(request)) + ), context: loadContext, params, }); @@ -81,9 +83,11 @@ export async function callRouteLoader({ singleFetch: boolean; }) { let result = await loader({ - request: singleFetch - ? stripRoutesParam(stripIndexParam(request)) - : stripDataParam(stripIndexParam(request)), + request: makeRequestSignalGcSafe( + singleFetch + ? stripRoutesParam(stripIndexParam(request)) + : stripDataParam(stripIndexParam(request)) + ), context: loadContext, params, }); @@ -113,6 +117,16 @@ export async function callRouteLoader({ return isResponse(result) ? result : json(result); } +function makeRequestSignalGcSafe(request: Request) { + // `undici` wraps the signal passed to the `Request` constructor. When the + // request object is garbage collected, the signal stops working. This is + // problematic when a loader or action is waiting for a signal to be aborted. + // To fix this, we hold a reference to the request in the signal itself so + // that the request isn't garbage collected while the signal is in use. + Object.defineProperty(request.signal, "__request", { value: request }); + return request; +} + // TODO: Document these search params better // and stop stripping these in V2. These break // support for running in a SW and also expose @@ -136,7 +150,7 @@ function stripIndexParam(request: Request) { method: request.method, body: request.body, headers: request.headers, - signal: request.signal, + signal: makeRequestSignalGcSafe(request).signal, }; if (init.body) { @@ -153,7 +167,7 @@ function stripDataParam(request: Request) { method: request.method, body: request.body, headers: request.headers, - signal: request.signal, + signal: makeRequestSignalGcSafe(request).signal, }; if (init.body) { @@ -170,7 +184,7 @@ function stripRoutesParam(request: Request) { method: request.method, body: request.body, headers: request.headers, - signal: request.signal, + signal: makeRequestSignalGcSafe(request).signal, }; if (init.body) {