From 3aa706feacb5d639b84b70082231d3767988c09a Mon Sep 17 00:00:00 2001 From: Mathieu Audebert Date: Sat, 10 Oct 2026 01:08:48 +0200 Subject: [PATCH] fix: patch react-router 8.4.0 so an aborted request cannot stop the server --- README.md | 14 +++ patches/react-router@8.4.0.patch | 162 +++++++++++++++++++++++++++++++ pnpm-lock.yaml | 41 ++++---- pnpm-workspace.yaml | 3 + 4 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 patches/react-router@8.4.0.patch diff --git a/README.md b/README.md index d5dfe5f..d7f8c61 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,18 @@ pnpm add payload @payloadcms/ui @payloadcms/translations @payloadcms/graphql gra pnpm add -D @react-router/dev@8.4.0 @vitejs/plugin-rsc@0.5.35 vite sass-embedded ``` +**Patch `react-router` 8.4.0.** In RSC mode, a client that aborts a document request can stop +the Node process (`TypeError: Invalid state: Unable to enqueue`, +[remix-run/react-router#15611](https://github.com/remix-run/react-router/issues/15611)). Until a +release contains [the fix](https://github.com/remix-run/react-router/pull/15612), copy +[`patches/react-router@8.4.0.patch`](patches/react-router@8.4.0.patch) into your app and add it to +`pnpm-workspace.yaml`: + +```yaml +patchedDependencies: + react-router@8.4.0: patches/react-router@8.4.0.patch +``` + ### 2. Switch the app to RSC Framework Mode ```ts @@ -263,6 +275,8 @@ A plain "SSR + loaders" React Router app cannot host the admin without changing ## Known limitations - React Router RSC mode is `unstable_`; pin versions and re-run the bench on upgrades. +- `react-router` 8.4.0 needs a patch, or an aborted request can stop the server: see + [Dependencies](#1-dependencies). - Mounting the admin at `/` (Payload's `admin-root` setup) is untested; TanStack skips it too. - The Payload CLI does not detect React Router: use `importMapFile` and `PAYLOAD_CONFIG_PATH`. - Each admin page is rendered to completion on the server before streaming starts (needed diff --git a/patches/react-router@8.4.0.patch b/patches/react-router@8.4.0.patch new file mode 100644 index 0000000..5a1cbd2 --- /dev/null +++ b/patches/react-router@8.4.0.patch @@ -0,0 +1,162 @@ +diff --git a/dist/development/lib/rsc/html-stream/server.js b/dist/development/lib/rsc/html-stream/server.js +index 326e0ea1136babf7a9ed03bb59087140069758bf..6804d2a45649ed37a2821b2aeb2efb37423e570e 100644 +--- a/dist/development/lib/rsc/html-stream/server.js ++++ b/dist/development/lib/rsc/html-stream/server.js +@@ -21,11 +21,31 @@ function injectRSCPayload(rscStream, { nonce } = {}) { + let rscReader = null; + let buffered = []; + let timeout = null; ++ async function stop(reason) { ++ cancelled = true; ++ if (timeout) { ++ clearTimeout(timeout); ++ timeout = null; ++ } ++ buffered.length = 0; ++ if (rscReader) await rscReader.cancel(reason).catch(() => {}); ++ else await rscStream.cancel(reason).catch(() => {}); ++ resolveFlightDataPromise(); ++ } ++ function tryEnqueue(controller, chunk) { ++ try { ++ controller.enqueue(chunk); ++ return true; ++ } catch (error) { ++ stop(error); ++ return false; ++ } ++ } + function flushBufferedChunks(controller) { + for (let chunk of buffered) { + let buf = decoder.decode(chunk, { stream: true }); + if (buf.endsWith(trailer)) buf = buf.slice(0, -14); +- controller.enqueue(encoder.encode(buf)); ++ if (!tryEnqueue(controller, encoder.encode(buf))) return; + } + buffered.length = 0; + timeout = null; +@@ -37,31 +57,32 @@ function injectRSCPayload(rscStream, { nonce } = {}) { + timeout = setTimeout(async () => { + if (cancelled) return; + flushBufferedChunks(controller); ++ if (cancelled) return; + if (!startedRSC) { + startedRSC = true; + rscReader = rscStream.getReader(); +- writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => controller.error(err)).then(resolveFlightDataPromise); ++ writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => { ++ if (cancelled) return; ++ try { ++ controller.error(err); ++ } catch (error) { ++ return stop(error); ++ } ++ }).then(resolveFlightDataPromise); + } + }, 0); + }, + async flush(controller) { + await flightDataPromise; ++ if (cancelled) return; + if (timeout) { + clearTimeout(timeout); + flushBufferedChunks(controller); + } +- controller.enqueue(encoder.encode("")); ++ tryEnqueue(controller, encoder.encode("")); + }, + async cancel(reason) { +- cancelled = true; +- if (timeout) { +- clearTimeout(timeout); +- timeout = null; +- } +- buffered.length = 0; +- if (rscReader) await rscReader.cancel(reason).catch(() => {}); +- else await rscStream.cancel(reason).catch(() => {}); +- resolveFlightDataPromise(); ++ await stop(reason); + } + }); + } +diff --git a/dist/production/lib/rsc/html-stream/server.js b/dist/production/lib/rsc/html-stream/server.js +index 326e0ea1136babf7a9ed03bb59087140069758bf..6804d2a45649ed37a2821b2aeb2efb37423e570e 100644 +--- a/dist/production/lib/rsc/html-stream/server.js ++++ b/dist/production/lib/rsc/html-stream/server.js +@@ -21,11 +21,31 @@ function injectRSCPayload(rscStream, { nonce } = {}) { + let rscReader = null; + let buffered = []; + let timeout = null; ++ async function stop(reason) { ++ cancelled = true; ++ if (timeout) { ++ clearTimeout(timeout); ++ timeout = null; ++ } ++ buffered.length = 0; ++ if (rscReader) await rscReader.cancel(reason).catch(() => {}); ++ else await rscStream.cancel(reason).catch(() => {}); ++ resolveFlightDataPromise(); ++ } ++ function tryEnqueue(controller, chunk) { ++ try { ++ controller.enqueue(chunk); ++ return true; ++ } catch (error) { ++ stop(error); ++ return false; ++ } ++ } + function flushBufferedChunks(controller) { + for (let chunk of buffered) { + let buf = decoder.decode(chunk, { stream: true }); + if (buf.endsWith(trailer)) buf = buf.slice(0, -14); +- controller.enqueue(encoder.encode(buf)); ++ if (!tryEnqueue(controller, encoder.encode(buf))) return; + } + buffered.length = 0; + timeout = null; +@@ -37,31 +57,32 @@ function injectRSCPayload(rscStream, { nonce } = {}) { + timeout = setTimeout(async () => { + if (cancelled) return; + flushBufferedChunks(controller); ++ if (cancelled) return; + if (!startedRSC) { + startedRSC = true; + rscReader = rscStream.getReader(); +- writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => controller.error(err)).then(resolveFlightDataPromise); ++ writeRSCStream(rscReader, controller, () => cancelled, nonce).catch((err) => { ++ if (cancelled) return; ++ try { ++ controller.error(err); ++ } catch (error) { ++ return stop(error); ++ } ++ }).then(resolveFlightDataPromise); + } + }, 0); + }, + async flush(controller) { + await flightDataPromise; ++ if (cancelled) return; + if (timeout) { + clearTimeout(timeout); + flushBufferedChunks(controller); + } +- controller.enqueue(encoder.encode("")); ++ tryEnqueue(controller, encoder.encode("")); + }, + async cancel(reason) { +- cancelled = true; +- if (timeout) { +- clearTimeout(timeout); +- timeout = null; +- } +- buffered.length = 0; +- if (rscReader) await rscReader.cancel(reason).catch(() => {}); +- else await rscStream.cancel(reason).catch(() => {}); +- resolveFlightDataPromise(); ++ await stop(reason); + } + }); + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d80d74b..9f47417 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + react-router@8.4.0: + hash: afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a + path: patches/react-router@8.4.0.patch + importers: .: @@ -50,7 +55,7 @@ importers: version: 19.3.0(react@19.3.0) react-router: specifier: 8.4.0 - version: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react-server-dom-webpack: specifier: 19.3.0 version: 19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)) @@ -83,7 +88,7 @@ importers: version: 4.0.0-canary.39(@types/react@19.2.14)(monaco-editor@0.57.0)(payload@4.0.0-canary.39(graphql@16.14.2)(typescript@5.9.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@5.9.3) '@react-router/serve': specifier: 8.4.0 - version: 8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) + version: 8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) graphql: specifier: ^16.8.1 version: 16.14.2 @@ -101,7 +106,7 @@ importers: version: 19.3.0(react@19.3.0) react-router: specifier: 8.4.0 - version: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react-server-dom-webpack: specifier: 19.3.0 version: 19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)) @@ -111,7 +116,7 @@ importers: version: 1.64.0 '@react-router/dev': specifier: 8.4.0 - version: 8.4.0(@react-router/serve@8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.35(react-dom@19.3.0(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(react@19.3.0)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15)))(babel-plugin-macros@3.1.0)(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(typescript@5.9.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15)) + version: 8.4.0(@react-router/serve@8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.35(react-dom@19.3.0(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(react@19.3.0)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15)))(babel-plugin-macros@3.1.0)(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(typescript@5.9.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15)) '@types/node': specifier: ^24.10.1 version: 24.19.1 @@ -4485,14 +4490,14 @@ snapshots: '@preact/signals-core@1.14.4': {} - '@react-router/dev@8.4.0(@react-router/serve@8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.35(react-dom@19.3.0(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(react@19.3.0)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15)))(babel-plugin-macros@3.1.0)(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(typescript@5.9.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15))': + '@react-router/dev@8.4.0(@react-router/serve@8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.35(react-dom@19.3.0(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(react@19.3.0)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15)))(babel-plugin-macros@3.1.0)(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(typescript@5.9.3)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15))': dependencies: '@babel/core': 7.29.7 '@babel/generator': 7.29.8 '@babel/parser': 7.29.9 '@babel/traverse': 7.29.10 '@babel/types': 7.29.8 - '@react-router/node': 8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) + '@react-router/node': 8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) '@remix-run/node-fetch-server': 0.14.2 babel-dead-code-elimination: 1.0.12 chokidar: 5.0.0 @@ -4507,13 +4512,13 @@ snapshots: picocolors: 1.1.1 pkg-types: 2.3.3 react-refresh: 0.18.0 - react-router: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react-router: 8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) semver: 7.8.5 tinyglobby: 0.2.17 valibot: 1.5.0(typescript@5.9.3) vite: 8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15) optionalDependencies: - '@react-router/serve': 8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) + '@react-router/serve': 8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) '@vitejs/plugin-rsc': 0.5.35(react-dom@19.3.0(react@19.3.0))(react-server-dom-webpack@19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)))(react@19.3.0)(vite@8.3.4(@types/node@24.19.1)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)(terser@5.51.2)(tsx@4.23.15)) react-server-dom-webpack: 19.3.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(webpack@5.111.1(esbuild@0.28.2)) typescript: 5.9.3 @@ -4521,30 +4526,30 @@ snapshots: - babel-plugin-macros - supports-color - '@react-router/express@8.4.0(express@5.2.1)(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3)': + '@react-router/express@8.4.0(express@5.2.1)(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3)': dependencies: - '@react-router/node': 8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) + '@react-router/node': 8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) express: 5.2.1 - react-router: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react-router: 8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) optionalDependencies: typescript: 5.9.3 - '@react-router/node@8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3)': + '@react-router/node@8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3)': dependencies: '@remix-run/node-fetch-server': 0.14.2 - react-router: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react-router: 8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) optionalDependencies: typescript: 5.9.3 - '@react-router/serve@8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3)': + '@react-router/serve@8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3)': dependencies: - '@react-router/express': 8.4.0(express@5.2.1)(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) - '@react-router/node': 8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) + '@react-router/express': 8.4.0(express@5.2.1)(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) + '@react-router/node': 8.4.0(react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@5.9.3) '@remix-run/node-fetch-server': 0.14.2 compression: 1.8.2 express: 5.2.1 morgan: 1.12.1 - react-router: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react-router: 8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) source-map-support: 0.5.21 transitivePeerDependencies: - supports-color @@ -6071,7 +6076,7 @@ snapshots: react-refresh@0.18.0: {} - react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + react-router@8.4.0(patch_hash=afd2b190ca052e317e859bb6af00b9147efd77353a4d2a3ee411772418b3961a)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: '@remix-run/route-pattern': 0.22.1 cookie-es: 3.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0b268fe..10e5180 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,3 +6,6 @@ onlyBuiltDependencies: - better-sqlite3 - esbuild - sharp + +patchedDependencies: + react-router@8.4.0: patches/react-router@8.4.0.patch