From 4291d2c9efcd6500d7b99093b09e198fbe4d9810 Mon Sep 17 00:00:00 2001 From: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:05:36 +0530 Subject: [PATCH 1/6] fix(router-core): read ssr.nonce when hydration scripts are framed --- .changeset/late-ssr-nonce.md | 5 +++ packages/router-core/src/ssr/STREAMING.md | 2 +- .../router-core/src/ssr/hydrationScripts.ts | 15 +++++++- packages/router-core/src/ssr/ssr-server.ts | 4 +- .../tests/transformStreamWithRouter.test.ts | 37 +++++++++++++++++++ 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 .changeset/late-ssr-nonce.md diff --git a/.changeset/late-ssr-nonce.md b/.changeset/late-ssr-nonce.md new file mode 100644 index 00000000000..7dd991ac6cf --- /dev/null +++ b/.changeset/late-ssr-nonce.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-core': patch +--- + +Apply an `ssr.nonce` set after the router's SSR utilities are attached (for example in the `createStartHandler` callback) to the hydration scripts again. diff --git a/packages/router-core/src/ssr/STREAMING.md b/packages/router-core/src/ssr/STREAMING.md index 12d3bd7b085..071aac91f9d 100644 --- a/packages/router-core/src/ssr/STREAMING.md +++ b/packages/router-core/src/ssr/STREAMING.md @@ -31,7 +31,7 @@ Seroval produces complete hydration sources. The hydration state owns these tasks: -- It stores the request nonce. +- It reads the request nonce from the router options when it frames scripts, so a nonce set after attachment still applies. - It creates the initial tags and the boundary. - It frames late records. - It keeps sources in FIFO order. diff --git a/packages/router-core/src/ssr/hydrationScripts.ts b/packages/router-core/src/ssr/hydrationScripts.ts index 79222e5d731..408e333ff98 100644 --- a/packages/router-core/src/ssr/hydrationScripts.ts +++ b/packages/router-core/src/ssr/hydrationScripts.ts @@ -178,7 +178,7 @@ class HydrationScriptsOwner implements HydrationScriptOutput { private opening: string | undefined constructor( - private readonly nonce: string | undefined, + private readonly nonceSource: HydrationScriptsNonce, initialSources?: ReadonlyArray, ) { // ServerSsr exposes this method as a bare callback, so keep this single @@ -199,6 +199,14 @@ class HydrationScriptsOwner implements HydrationScriptOutput { return this.outputState } + // Read on use: an app may set `ssr.nonce` after the SSR utilities are + // attached, e.g. in the `createStartHandler` callback. + private get nonce() { + return typeof this.nonceSource === 'function' + ? this.nonceSource() + : this.nonceSource + } + get error() { return this.outputError } @@ -680,9 +688,12 @@ class HydrationScriptsOwner implements HydrationScriptOutput { } } +/** A nonce, or a getter that reads it when the first script is framed. */ +type HydrationScriptsNonce = string | undefined | (() => string | undefined) + /** Create the hydration-script owner for one server request. */ export function createHydrationScripts( - nonce: string | undefined, + nonce: HydrationScriptsNonce, initialSources?: ReadonlyArray, ): HydrationScripts { return new HydrationScriptsOwner(nonce, initialSources) diff --git a/packages/router-core/src/ssr/ssr-server.ts b/packages/router-core/src/ssr/ssr-server.ts index e7c9cc2fa6a..6752d75c8f7 100644 --- a/packages/router-core/src/ssr/ssr-server.ts +++ b/packages/router-core/src/ssr/ssr-server.ts @@ -403,7 +403,9 @@ export function attachRouterServerSsrUtils({ // still be pending when the response ends. let settled = false let disposeSerialization: (() => void) | undefined - const hydrationScripts = createHydrationScripts(router.options.ssr?.nonce) + const hydrationScripts = createHydrationScripts( + () => router.options.ssr?.nonce, + ) const serverSsr: ServerSsr = { hydrationScripts, diff --git a/packages/router-core/tests/transformStreamWithRouter.test.ts b/packages/router-core/tests/transformStreamWithRouter.test.ts index e5ce1d8d50d..3adf3180324 100644 --- a/packages/router-core/tests/transformStreamWithRouter.test.ts +++ b/packages/router-core/tests/transformStreamWithRouter.test.ts @@ -477,6 +477,43 @@ describe('transformReadableStreamWithRouter — real SSR scripts', () => { ) }) + test('uses an ssr nonce set after the SSR utilities are attached', async () => { + const streamed = createDeferred() + const router = createRealSsrRouter({ streamed: streamed.promise }) + attachRouterServerSsrUtils({ router, manifest: undefined }) + // Apps may set the nonce later, e.g. in the createStartHandler callback. + router.options.ssr = { ...router.options.ssr, nonce: 'late' } + + await router.load() + await router.serverSsr!.dehydrate() + + const initialScripts = router.serverSsr!.takeInitialHydrationScriptTags()! + expect(initialScripts.before.length).toBeGreaterThan(0) + for (const tag of [...initialScripts.before, initialScripts.boundary]) { + expect(tag.attrs?.nonce).toBe('late') + } + + const upstream = makeManualUpstream() + const output = transformReadableStreamWithRouter(router, upstream.stream) + upstream.push( + `
app
${renderInitialScripts(initialScripts)}`, + ) + streamed.resolve('done') + await flush(20) + upstream.push('') + upstream.close() + + const html = await readAll(output) + const streamEnd = html.lastIndexOf(`${GLOBAL_TSR}.e()`) + expect(streamEnd).toBeGreaterThan(-1) + expect( + html.lastIndexOf('