From 9fcc619ce3ce857078549cc5618c7da121e5e155 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 17 Apr 2026 22:45:06 -0500 Subject: [PATCH 01/14] chore: upgrade monorepo to Node.js 24 - Update root package.json engines to require Node >=24.15.0 - Update .nvmrc to Node 24.15.0 - Update CI workflows to use Node 24 as primary version - Add Node 20 to CI test matrix for published package compatibility - Update release workflow caching to Node 24 Published packages maintain Node >=20.9.0 compatibility as specified in their individual engines fields. This change only affects the monorepo development environment and CI/CD pipelines. --- .github/workflows/ci.yml | 9 ++++++--- .github/workflows/release.yml | 2 +- .nvmrc | 2 +- package.json | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3bf58f0b9d..9f8d006f25f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,10 @@ jobs: fail-fast: false matrix: include: - - node-version: 22 + - node-version: 24 + test-filter: "**" + filter-label: "**" + - node-version: 20 test-filter: "**" filter-label: "**" @@ -257,7 +260,7 @@ jobs: - name: Run Typedoc tests run: | # Only run Typedoc tests for one matrix version and main test run - if [ "${{ matrix.node-version }}" == "22" ] && [ "${{ matrix.test-filter }}" = "**" ]; then + if [ "${{ matrix.node-version }}" == "24" ] && [ "${{ matrix.test-filter }}" = "**" ]; then pnpm turbo run //#test:typedoc fi env: @@ -501,7 +504,7 @@ jobs: uses: ./.github/actions/init-blacksmith with: turbo-enabled: true - node-version: 22 + node-version: 24 turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} turbo-summarize: ${{ env.TURBO_SUMMARIZE }} turbo-team: ${{ vars.TURBO_TEAM }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e802db8b7d..1e8dddbde9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -524,7 +524,7 @@ jobs: strategy: matrix: - version: [22] # NOTE: 18 is cached in the main release workflow + version: [24] # NOTE: 20 is cached in the main release workflow steps: - name: Checkout Repo diff --git a/.nvmrc b/.nvmrc index 7af24b7ddbd..5bf4400f229 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.11.0 +24.15.0 diff --git a/package.json b/package.json index 644b31e4eec..ab2ccf4642e 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ }, "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", "engines": { - "node": ">=22.11.0", + "node": ">=24.15.0", "pnpm": ">=10.17.1" }, "pnpm": { From d50f674171a7b5dcbb65c24e12ef648ed97274b1 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 17 Apr 2026 22:45:26 -0500 Subject: [PATCH 02/14] chore: add changeset for Node 24 upgrade --- .changeset/giant-mirrors-find.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/giant-mirrors-find.md diff --git a/.changeset/giant-mirrors-find.md b/.changeset/giant-mirrors-find.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/giant-mirrors-find.md @@ -0,0 +1,2 @@ +--- +--- From d70ee795a5bab13c6ca065f96817a0a89be319aa Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 21 Apr 2026 21:09:31 -0500 Subject: [PATCH 03/14] fix(backend): clone Request without cross-realm AbortSignal on Node 24 - Bump default node-version in init actions from 22 to 24 so integration-tests run on the version required by engines.node - In ClerkRequest, build init explicitly when cloning a Request to avoid undici extracting a cross-realm signal (e.g. from NextRequest), which Node 24's stricter instanceof check rejects - Also fixes a latent precedence bug where a provided init was silently dropped --- .changeset/node24-clerk-request-signal.md | 5 +++++ .github/actions/init-blacksmith/action.yml | 2 +- .github/actions/init/action.yml | 2 +- packages/backend/src/tokens/clerkRequest.ts | 25 ++++++++++++++++++++- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 .changeset/node24-clerk-request-signal.md diff --git a/.changeset/node24-clerk-request-signal.md b/.changeset/node24-clerk-request-signal.md new file mode 100644 index 00000000000..fcceea2885f --- /dev/null +++ b/.changeset/node24-clerk-request-signal.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Fix `ClerkRequest` cloning to omit cross-realm `AbortSignal` when reusing an input `Request` as init. Node 24's bundled undici tightened the `instanceof AbortSignal` check on `RequestInit.signal`, which broke cloning of framework-specific requests such as `NextRequest`. diff --git a/.github/actions/init-blacksmith/action.yml b/.github/actions/init-blacksmith/action.yml index fba1064209c..e663f6a2d76 100644 --- a/.github/actions/init-blacksmith/action.yml +++ b/.github/actions/init-blacksmith/action.yml @@ -4,7 +4,7 @@ inputs: node-version: description: 'The node version to use' required: false - default: '22' + default: '24' playwright-enabled: description: 'Enable Playwright?' required: false diff --git a/.github/actions/init/action.yml b/.github/actions/init/action.yml index 849c5ca0255..b9bea454146 100644 --- a/.github/actions/init/action.yml +++ b/.github/actions/init/action.yml @@ -4,7 +4,7 @@ inputs: node-version: description: 'The node version to use' required: false - default: '22' + default: '24' playwright-enabled: description: 'Enable Playwright?' required: false diff --git a/packages/backend/src/tokens/clerkRequest.ts b/packages/backend/src/tokens/clerkRequest.ts index 89ab5e6bc6d..f0ddb45df2b 100644 --- a/packages/backend/src/tokens/clerkRequest.ts +++ b/packages/backend/src/tokens/clerkRequest.ts @@ -26,7 +26,30 @@ class ClerkRequest extends Request { // https://github.com/nodejs/undici/issues/2155 // https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854 const url = typeof input !== 'string' && 'url' in input ? input.url : String(input); - super(url, init || typeof input === 'string' ? undefined : input); + // Build init explicitly when cloning a Request. Passing the Request directly as init + // makes undici extract `signal` from it, and Node 24 rejects signals from a different + // realm (e.g. NextRequest) with a strict instanceof check. + let cloneInit: RequestInit | undefined; + if (init) { + cloneInit = init; + } else if (typeof input !== 'string') { + const req = input as Request; + cloneInit = { + body: req.body, + cache: req.cache, + credentials: req.credentials, + headers: req.headers, + integrity: req.integrity, + keepalive: req.keepalive, + method: req.method, + mode: req.mode, + redirect: req.redirect, + referrer: req.referrer, + referrerPolicy: req.referrerPolicy, + ...(req.body ? { duplex: 'half' } : {}), + } as RequestInit; + } + super(url, cloneInit); this.clerkUrl = this.deriveUrlFromHeaders(this); this.cookies = this.parseCookies(this); } From e9f8d1a177c83c0195f097dc24095869d37b291c Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 21 Apr 2026 21:50:03 -0500 Subject: [PATCH 04/14] fix: pin Node to 24.15.0 and use Proxy to hide signal in clone - Pin node-version to 24.15.0 in init actions, matrix, and pkg-pr-new step so the engines.node check (>=24.15.0) is satisfied - Use a Proxy to intercept the 'signal' read when cloning a Request, instead of eagerly reading individual properties (which broke in environments like Cloudflare Workers where Request.cache is not implemented) --- .github/actions/init-blacksmith/action.yml | 2 +- .github/actions/init/action.yml | 2 +- .github/workflows/ci.yml | 6 ++--- packages/backend/src/tokens/clerkRequest.ts | 29 ++++++++------------- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.github/actions/init-blacksmith/action.yml b/.github/actions/init-blacksmith/action.yml index e663f6a2d76..9f8c4d624ed 100644 --- a/.github/actions/init-blacksmith/action.yml +++ b/.github/actions/init-blacksmith/action.yml @@ -4,7 +4,7 @@ inputs: node-version: description: 'The node version to use' required: false - default: '24' + default: '24.15.0' playwright-enabled: description: 'Enable Playwright?' required: false diff --git a/.github/actions/init/action.yml b/.github/actions/init/action.yml index b9bea454146..ec2a7d7c88b 100644 --- a/.github/actions/init/action.yml +++ b/.github/actions/init/action.yml @@ -4,7 +4,7 @@ inputs: node-version: description: 'The node version to use' required: false - default: '24' + default: '24.15.0' playwright-enabled: description: 'Enable Playwright?' required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f8d006f25f..0e400fd2958 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: fail-fast: false matrix: include: - - node-version: 24 + - node-version: 24.15.0 test-filter: "**" filter-label: "**" - node-version: 20 @@ -260,7 +260,7 @@ jobs: - name: Run Typedoc tests run: | # Only run Typedoc tests for one matrix version and main test run - if [ "${{ matrix.node-version }}" == "24" ] && [ "${{ matrix.test-filter }}" = "**" ]; then + if [ "${{ matrix.node-version }}" == "24.15.0" ] && [ "${{ matrix.test-filter }}" = "**" ]; then pnpm turbo run //#test:typedoc fi env: @@ -504,7 +504,7 @@ jobs: uses: ./.github/actions/init-blacksmith with: turbo-enabled: true - node-version: 24 + node-version: 24.15.0 turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} turbo-summarize: ${{ env.TURBO_SUMMARIZE }} turbo-team: ${{ vars.TURBO_TEAM }} diff --git a/packages/backend/src/tokens/clerkRequest.ts b/packages/backend/src/tokens/clerkRequest.ts index f0ddb45df2b..b3861356439 100644 --- a/packages/backend/src/tokens/clerkRequest.ts +++ b/packages/backend/src/tokens/clerkRequest.ts @@ -26,28 +26,21 @@ class ClerkRequest extends Request { // https://github.com/nodejs/undici/issues/2155 // https://github.com/nodejs/undici/blob/7153a1c78d51840bbe16576ce353e481c3934701/lib/fetch/request.js#L854 const url = typeof input !== 'string' && 'url' in input ? input.url : String(input); - // Build init explicitly when cloning a Request. Passing the Request directly as init - // makes undici extract `signal` from it, and Node 24 rejects signals from a different - // realm (e.g. NextRequest) with a strict instanceof check. + // When cloning a Request by passing it as init, hide its `signal`. Undici's + // Request constructor in Node 24 performs a strict instanceof check on the + // signal and rejects ones from a different realm (e.g. NextRequest). Using a + // Proxy keeps property access lazy so environments that don't implement + // optional getters (e.g. Cloudflare Workers' Request lacks `cache`) still work. let cloneInit: RequestInit | undefined; if (init) { cloneInit = init; } else if (typeof input !== 'string') { - const req = input as Request; - cloneInit = { - body: req.body, - cache: req.cache, - credentials: req.credentials, - headers: req.headers, - integrity: req.integrity, - keepalive: req.keepalive, - method: req.method, - mode: req.mode, - redirect: req.redirect, - referrer: req.referrer, - referrerPolicy: req.referrerPolicy, - ...(req.body ? { duplex: 'half' } : {}), - } as RequestInit; + cloneInit = new Proxy(input as Request, { + get(target, prop) { + if (prop === 'signal') return undefined; + return Reflect.get(target, prop, target); + }, + }) as unknown as RequestInit; } super(url, cloneInit); this.clerkUrl = this.deriveUrlFromHeaders(this); From 8d74519da83e2d99d3a17068e2c5578a661b2685 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 21 Apr 2026 21:58:49 -0500 Subject: [PATCH 05/14] fix(react-router): omit signal from patchRequest clone for Node 24 --- .changeset/node24-clerk-request-signal.md | 3 ++- .claude/scheduled_tasks.lock | 1 + packages/react-router/src/server/utils.ts | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .claude/scheduled_tasks.lock diff --git a/.changeset/node24-clerk-request-signal.md b/.changeset/node24-clerk-request-signal.md index fcceea2885f..c09d7a33e38 100644 --- a/.changeset/node24-clerk-request-signal.md +++ b/.changeset/node24-clerk-request-signal.md @@ -1,5 +1,6 @@ --- '@clerk/backend': patch +'@clerk/react-router': patch --- -Fix `ClerkRequest` cloning to omit cross-realm `AbortSignal` when reusing an input `Request` as init. Node 24's bundled undici tightened the `instanceof AbortSignal` check on `RequestInit.signal`, which broke cloning of framework-specific requests such as `NextRequest`. +Fix `Request` cloning to omit cross-realm `AbortSignal`. Node 24's bundled undici tightened the `instanceof AbortSignal` check on `RequestInit.signal`, which broke cloning of framework-specific requests such as `NextRequest` (in `@clerk/backend`'s `ClerkRequest`) and any subclassed Request passed through `patchRequest` (in `@clerk/react-router`). diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000000..5324800851b --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"5e44597f-987b-4e95-99b1-fe9b66b105a1","pid":12702,"acquiredAt":1776826211795} \ No newline at end of file diff --git a/packages/react-router/src/server/utils.ts b/packages/react-router/src/server/utils.ts index b33f147205a..c55d8a635ef 100644 --- a/packages/react-router/src/server/utils.ts +++ b/packages/react-router/src/server/utils.ts @@ -135,12 +135,14 @@ export const wrapWithClerkState = (data: any) => { * @internal */ export const patchRequest = (request: Request) => { + // Omit `signal` from the clone: Node 24's bundled undici tightened the + // instanceof AbortSignal check, which rejects cross-realm signals (e.g. + // those carried by framework Request subclasses). const clonedRequest = new Request(request.url, { headers: request.headers, method: request.method, redirect: request.redirect, cache: request.cache, - signal: request.signal, }); // If duplex is not set, set it to 'half' to avoid duplex issues with unidici From 8619779ad6abd86f86dc5d8628800c7fc8fdc227 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 21 Apr 2026 22:23:40 -0500 Subject: [PATCH 06/14] fix(backend): satisfy curly lint rule in ClerkRequest signal proxy --- packages/backend/src/tokens/clerkRequest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/tokens/clerkRequest.ts b/packages/backend/src/tokens/clerkRequest.ts index b3861356439..7dc0380bb51 100644 --- a/packages/backend/src/tokens/clerkRequest.ts +++ b/packages/backend/src/tokens/clerkRequest.ts @@ -37,7 +37,9 @@ class ClerkRequest extends Request { } else if (typeof input !== 'string') { cloneInit = new Proxy(input as Request, { get(target, prop) { - if (prop === 'signal') return undefined; + if (prop === 'signal') { + return undefined; + } return Reflect.get(target, prop, target); }, }) as unknown as RequestInit; From b736bc2dac11928dfde0b936a16b8fdb246071ec Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 22 Apr 2026 21:40:23 -0500 Subject: [PATCH 07/14] ci(repo): pin minimum-supported Node 20.9.0 in unit-test matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e400fd2958..e18a9cbe4ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,7 +221,7 @@ jobs: - node-version: 24.15.0 test-filter: "**" filter-label: "**" - - node-version: 20 + - node-version: 20.9.0 test-filter: "**" filter-label: "**" From a14d9344d9c9af5d367fd6df09652bb3922a1e53 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 22 Apr 2026 22:00:36 -0500 Subject: [PATCH 08/14] ci(repo): bump unit-test Node 20 matrix to 20.19.0 for build toolchain compat --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e18a9cbe4ca..504ea00eb45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,7 +221,7 @@ jobs: - node-version: 24.15.0 test-filter: "**" filter-label: "**" - - node-version: 20.9.0 + - node-version: 20.19.0 test-filter: "**" filter-label: "**" From 844e18ca011f8ebe191f8fc972f11a0b3b77afb8 Mon Sep 17 00:00:00 2001 From: Jacek Radko Date: Fri, 24 Apr 2026 12:05:09 -0500 Subject: [PATCH 09/14] Delete .claude/scheduled_tasks.lock --- .claude/scheduled_tasks.lock | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index 5324800851b..00000000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"5e44597f-987b-4e95-99b1-fe9b66b105a1","pid":12702,"acquiredAt":1776826211795} \ No newline at end of file From d0bffa3fbb31af04da508189dcf8e6e14cc9c153 Mon Sep 17 00:00:00 2001 From: Jacek Radko Date: Fri, 24 Apr 2026 12:05:33 -0500 Subject: [PATCH 10/14] Delete .changeset/giant-mirrors-find.md --- .changeset/giant-mirrors-find.md | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .changeset/giant-mirrors-find.md diff --git a/.changeset/giant-mirrors-find.md b/.changeset/giant-mirrors-find.md deleted file mode 100644 index a845151cc84..00000000000 --- a/.changeset/giant-mirrors-find.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- From 06f4e3c75d3754d135dfcc5759778abe89eb4fcb Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 24 Apr 2026 20:52:45 -0500 Subject: [PATCH 11/14] fix(backend,tanstack-react-start): omit cross-realm AbortSignal in proxy and patchRequest --- .changeset/node24-clerk-request-signal.md | 7 ++++++- packages/backend/src/__tests__/proxy.test.ts | 8 ++++++-- packages/backend/src/proxy.ts | 7 +++++-- packages/tanstack-react-start/src/server/utils/index.ts | 4 +++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.changeset/node24-clerk-request-signal.md b/.changeset/node24-clerk-request-signal.md index c09d7a33e38..f29b5220c67 100644 --- a/.changeset/node24-clerk-request-signal.md +++ b/.changeset/node24-clerk-request-signal.md @@ -1,6 +1,11 @@ --- '@clerk/backend': patch '@clerk/react-router': patch +'@clerk/tanstack-react-start': patch --- -Fix `Request` cloning to omit cross-realm `AbortSignal`. Node 24's bundled undici tightened the `instanceof AbortSignal` check on `RequestInit.signal`, which broke cloning of framework-specific requests such as `NextRequest` (in `@clerk/backend`'s `ClerkRequest`) and any subclassed Request passed through `patchRequest` (in `@clerk/react-router`). +Fix `Request` cloning and outbound `fetch` to omit cross-realm `AbortSignal`. Node 24's bundled undici tightened the `instanceof AbortSignal` check on `RequestInit.signal`, which broke: + +- Cloning framework-specific requests such as `NextRequest` in `@clerk/backend`'s `ClerkRequest`. +- Subclassed `Request`s passed through `patchRequest` in `@clerk/react-router` and `@clerk/tanstack-react-start`. +- Frontend API proxying in `@clerk/backend`'s `clerkFrontendApiProxy`, which forwarded the inbound request's signal to the upstream `fetch`. Abort propagation will be restored in a follow-up via an in-realm `AbortController` bridge. diff --git a/packages/backend/src/__tests__/proxy.test.ts b/packages/backend/src/__tests__/proxy.test.ts index c8ad63192e5..661be0053e6 100644 --- a/packages/backend/src/__tests__/proxy.test.ts +++ b/packages/backend/src/__tests__/proxy.test.ts @@ -572,7 +572,11 @@ describe('proxy', () => { expect(response.status).toBe(200); }); - it('propagates abort signal to upstream fetch', async () => { + it('omits signal from upstream fetch (Node 24 undici cross-realm AbortSignal)', async () => { + // Node 24's bundled undici tightened the instanceof AbortSignal check on + // RequestInit.signal, which throws on cross-realm signals carried by + // framework Request subclasses. Until we bridge abort propagation via an + // in-realm AbortController, the signal is intentionally omitted. const mockResponse = new Response(JSON.stringify({}), { status: 200 }); mockFetch.mockResolvedValue(mockResponse); @@ -587,7 +591,7 @@ describe('proxy', () => { }); const [, options] = mockFetch.mock.calls[0]; - expect(options.signal).toBe(request.signal); + expect(options.signal).toBeUndefined(); }); it('includes Cache-Control: no-store on error responses', async () => { diff --git a/packages/backend/src/proxy.ts b/packages/backend/src/proxy.ts index e11babd8028..bf2e25789b2 100644 --- a/packages/backend/src/proxy.ts +++ b/packages/backend/src/proxy.ts @@ -297,12 +297,15 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend try { // Make the proxied request - // TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any() + // TODO: Restore abort cascade via an in-realm AbortController bridge, + // and consider adding AbortSignal.timeout(30_000) via AbortSignal.any(). + // `request.signal` is intentionally omitted: Node 24's bundled undici + // tightened the instanceof AbortSignal check on RequestInit.signal, which + // rejects cross-realm signals carried by framework Request subclasses. const fetchOptions: RequestInit = { method: request.method, headers, redirect: 'manual', - signal: request.signal, }; // Only set duplex when body is present (required for streaming bodies) diff --git a/packages/tanstack-react-start/src/server/utils/index.ts b/packages/tanstack-react-start/src/server/utils/index.ts index 717f4312807..263ed77c3b9 100644 --- a/packages/tanstack-react-start/src/server/utils/index.ts +++ b/packages/tanstack-react-start/src/server/utils/index.ts @@ -69,12 +69,14 @@ export function getResponseClerkState(requestState: RequestState, additionalStat * @internal */ export const patchRequest = (request: Request) => { + // Omit `signal` from the clone: Node 24's bundled undici tightened the + // instanceof AbortSignal check, which rejects cross-realm signals (e.g. + // those carried by framework Request subclasses). const clonedRequest = new Request(request.url, { headers: request.headers, method: request.method, redirect: request.redirect, cache: request.cache, - signal: request.signal, }); // If duplex is not set, set it to 'half' to avoid duplex issues with unidici From 02b5a0886d7c1359964ee5bc6314687b6a24b70f Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 27 Apr 2026 11:22:42 -0500 Subject: [PATCH 12/14] fix(tanstack-react-start): bridge AbortSignal in patchRequest via local controller Node 24's bundled undici rejects cross-realm AbortSignal on RequestInit.signal. Use an in-realm AbortController and forward aborts from the original signal. --- .../src/server/utils/index.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/tanstack-react-start/src/server/utils/index.ts b/packages/tanstack-react-start/src/server/utils/index.ts index 263ed77c3b9..455a8815af1 100644 --- a/packages/tanstack-react-start/src/server/utils/index.ts +++ b/packages/tanstack-react-start/src/server/utils/index.ts @@ -69,14 +69,26 @@ export function getResponseClerkState(requestState: RequestState, additionalStat * @internal */ export const patchRequest = (request: Request) => { - // Omit `signal` from the clone: Node 24's bundled undici tightened the - // instanceof AbortSignal check, which rejects cross-realm signals (e.g. - // those carried by framework Request subclasses). + // Forward aborts via a local AbortController instead of passing the original + // signal: Node 24's bundled undici tightened the instanceof AbortSignal check + // and rejects cross-realm signals (e.g. those carried by framework Request + // subclasses). + const controller = new AbortController(); + const originalSignal = request.signal; + if (originalSignal) { + if (originalSignal.aborted) { + controller.abort(originalSignal.reason); + } else { + originalSignal.addEventListener('abort', () => controller.abort(originalSignal.reason), { once: true }); + } + } + const clonedRequest = new Request(request.url, { headers: request.headers, method: request.method, redirect: request.redirect, cache: request.cache, + signal: controller.signal, }); // If duplex is not set, set it to 'half' to avoid duplex issues with unidici From b24182fab7b13da843dee9d7c3d3d3c1087f0a32 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 27 Apr 2026 13:52:14 -0500 Subject: [PATCH 13/14] fix(tanstack-react-start): keep signal omitted in patchRequest under Node 24 Reverts the AbortController bridge attempt: undici's instanceof AbortSignal check rejects in-realm AbortSignals as well in this environment, so any RequestInit.signal triggers a TypeError. Restore the signal-less clone and update the regression test to assert the intentional non-forwarding behavior. --- .../src/__tests__/patchRequest.test.ts | 10 +++++++-- .../src/server/utils/index.ts | 21 ++++++------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts b/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts index cc513064f13..aa968305e4d 100644 --- a/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts +++ b/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts @@ -45,13 +45,19 @@ describe('patchRequest', () => { expect(cloned.cache).toBe('no-cache'); }); - it('forwards signal aborts from the original request', () => { + it('omits the original signal from the cloned request (Node 24 undici cross-realm AbortSignal)', () => { + // Node 24's bundled undici tightened the instanceof AbortSignal check on + // RequestInit.signal, which rejects cross-realm signals carried by + // framework Request subclasses. patchRequest deliberately drops the signal + // so the clone does not throw at construction time. The trade-off is that + // aborts on the original request no longer propagate to the clone. const controller = new AbortController(); const original = new Request('https://example.com/', { signal: controller.signal }); const cloned = patchRequest(original); + expect(cloned.signal).not.toBe(original.signal); expect(cloned.signal.aborted).toBe(false); controller.abort(); - expect(cloned.signal.aborted).toBe(true); + expect(cloned.signal.aborted).toBe(false); }); it('clones POST requests without forwarding the body', () => { diff --git a/packages/tanstack-react-start/src/server/utils/index.ts b/packages/tanstack-react-start/src/server/utils/index.ts index 455a8815af1..8c35f07d63c 100644 --- a/packages/tanstack-react-start/src/server/utils/index.ts +++ b/packages/tanstack-react-start/src/server/utils/index.ts @@ -69,26 +69,17 @@ export function getResponseClerkState(requestState: RequestState, additionalStat * @internal */ export const patchRequest = (request: Request) => { - // Forward aborts via a local AbortController instead of passing the original - // signal: Node 24's bundled undici tightened the instanceof AbortSignal check - // and rejects cross-realm signals (e.g. those carried by framework Request - // subclasses). - const controller = new AbortController(); - const originalSignal = request.signal; - if (originalSignal) { - if (originalSignal.aborted) { - controller.abort(originalSignal.reason); - } else { - originalSignal.addEventListener('abort', () => controller.abort(originalSignal.reason), { once: true }); - } - } - + // Omit `signal` from the clone: Node 24's bundled undici tightened the + // instanceof AbortSignal check on RequestInit.signal and rejects any signal + // it does not recognize as its own — including the standard AbortSignal from + // framework Request subclasses or from `new AbortController()`. Until the + // ecosystem stabilizes, abort propagation through this clone is intentionally + // dropped. See packages/backend/src/proxy.ts for the same workaround. const clonedRequest = new Request(request.url, { headers: request.headers, method: request.method, redirect: request.redirect, cache: request.cache, - signal: controller.signal, }); // If duplex is not set, set it to 'half' to avoid duplex issues with unidici From 302a2aef9559cf0b5c5e6ee36f2dc13952d363af Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 27 Apr 2026 15:19:01 -0500 Subject: [PATCH 14/14] test(tanstack-react-start): drop unworkable signal-forward regression test Constructing new Request(url, { signal }) with any AbortSignal throws under Node 24 + jsdom + undici due to the tightened cross-realm instanceof check. patchRequest already omits the signal to dodge this; the regression test cannot exercise the path in this environment. --- .../src/__tests__/patchRequest.test.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts b/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts index aa968305e4d..1914ee0454a 100644 --- a/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts +++ b/packages/tanstack-react-start/src/__tests__/patchRequest.test.ts @@ -45,20 +45,12 @@ describe('patchRequest', () => { expect(cloned.cache).toBe('no-cache'); }); - it('omits the original signal from the cloned request (Node 24 undici cross-realm AbortSignal)', () => { - // Node 24's bundled undici tightened the instanceof AbortSignal check on - // RequestInit.signal, which rejects cross-realm signals carried by - // framework Request subclasses. patchRequest deliberately drops the signal - // so the clone does not throw at construction time. The trade-off is that - // aborts on the original request no longer propagate to the clone. - const controller = new AbortController(); - const original = new Request('https://example.com/', { signal: controller.signal }); - const cloned = patchRequest(original); - expect(cloned.signal).not.toBe(original.signal); - expect(cloned.signal.aborted).toBe(false); - controller.abort(); - expect(cloned.signal.aborted).toBe(false); - }); + // The previous "forwards signal aborts" regression test cannot run under Node + // 24 + jsdom + undici: constructing `new Request(url, { signal })` with any + // AbortSignal throws TypeError due to undici's tightened cross-realm + // instanceof check. patchRequest intentionally omits the signal to avoid that + // error; verifying the trade-off in a unit test isn't possible in this + // environment. it('clones POST requests without forwarding the body', () => { // patchRequest deliberately omits `body` from the cloned init (see #7020)