diff --git a/.changeset/silly-gifts-wash.md b/.changeset/silly-gifts-wash.md new file mode 100644 index 00000000000..f17c9084b4d --- /dev/null +++ b/.changeset/silly-gifts-wash.md @@ -0,0 +1,6 @@ +--- +"@remix-run/dev": patch +"@remix-run/express": patch +--- + +Fix adapter logic for aborting `request.signal` so we don't incorrectly abort on the `close` event for successful requests diff --git a/packages/remix-dev/vite/node-adapter.ts b/packages/remix-dev/vite/node-adapter.ts index 2783da81778..40310607b0b 100644 --- a/packages/remix-dev/vite/node-adapter.ts +++ b/packages/remix-dev/vite/node-adapter.ts @@ -46,10 +46,7 @@ export function fromNodeRequest( ); let url = new URL(nodeReq.originalUrl, origin); - // Abort action/loaders once we can no longer write a response - let controller = new AbortController(); - nodeRes.on("close", () => controller.abort()); - + let controller: AbortController | null = new AbortController(); let init: RequestInit = { method: nodeReq.method, headers: fromNodeHeaders(nodeReq.headers), @@ -61,6 +58,13 @@ export function fromNodeRequest( (init as { duplex: "half" }).duplex = "half"; } + // Abort action/loaders once we can no longer write a response iff we have + // not yet sent a response (i.e., `close` without `finish`) + // `finish` -> done rendering the response + // `close` -> response can no longer be written to + nodeRes.on("finish", () => (controller = null)); + nodeRes.on("close", () => controller?.abort()); + return new Request(url.href, init); } diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index 3c0d25bc404..166d3654f31 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -97,10 +97,7 @@ export function createRemixRequest( // Use `req.originalUrl` so Remix is aware of the full path let url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`); - // Abort action/loaders once we can no longer write a response - let controller = new AbortController(); - res.on("close", () => controller.abort()); - + let controller: AbortController | null = new AbortController(); let init: RequestInit = { method: req.method, headers: createRemixHeaders(req.headers), @@ -112,6 +109,13 @@ export function createRemixRequest( (init as { duplex: "half" }).duplex = "half"; } + // Abort action/loaders once we can no longer write a response iff we have + // not yet sent a response (i.e., `close` without `finish`) + // `finish` -> done rendering the response + // `close` -> response can no longer be written to + res.on("finish", () => (controller = null)); + res.on("close", () => controller?.abort()); + return new Request(url.href, init); }