Skip to content

fix: fix streaming#7497

Merged
schiller-manuel merged 2 commits into
mainfrom
fix-streaming
May 29, 2026
Merged

fix: fix streaming#7497
schiller-manuel merged 2 commits into
mainfrom
fix-streaming

Conversation

@schiller-manuel
Copy link
Copy Markdown
Collaborator

@schiller-manuel schiller-manuel commented May 28, 2026

fixes #7402

Summary by CodeRabbit

  • Bug Fixes

    • Improved SSR streaming reliability for request aborts/cancellations across React, Vue, and Solid
    • Fixed HTML/script injection ordering and buffering to prevent mid-tag injection
    • Hardened query-stream teardown, cache cleanup, and in-flight query handling
    • Addressed backpressure issues to bound memory during streaming
  • New Features

    • Added SSR fast-path streaming mode for lower-latency passthrough
    • Exposed richer SSR lifecycle hooks for finer cleanup/control
  • Tests

    • Added extensive SSR streaming, cleanup, backpressure, and e2e query-stream tests

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2935563a-e4f8-470a-8fcb-c22d12005e1b

📥 Commits

Reviewing files that changed from the base of the PR and between f680d15 and 0c0a6cc.

📒 Files selected for processing (3)
  • e2e/react-start/streaming-ssr/tests/query-heavy.spec.ts
  • packages/router-core/src/ssr/handlerCallback.ts
  • packages/router-core/tests/transformStreamWithRouter.test.ts
💤 Files with no reviewable changes (1)
  • packages/router-core/src/ssr/handlerCallback.ts

📝 Walkthrough

Walkthrough

Adds SSR-aware response types and normalization, refactors server-SSR lifecycle and script buffering, implements a backpressure-safe transformStreamWithRouter (fast/main paths and HTML boundary detection), wires abort/cleanup into React/Solid/Vue/start-server flows, strengthens query SSR teardown, and adds extensive tests/benchmarks.

Changes

SSR Streaming, Abort Handling & Cleanup

Layer / File(s) Summary
SSR Response Types & Handler Contract
packages/router-core/src/ssr/handlerCallback.ts, packages/router-core/src/ssr/server.ts
SsrResponse/HandlerCallbackResult added; isSsrResponse/normalizeSsrResponse; createSsrStreamResponse enforces body + idempotent dispose; replaceSsrResponse & stripSsrResponseBody manage body replacement and cleanup downgrade.
Router SSR & Lifecycle Interfaces
packages/router-core/src/router.ts
ServerSsr adds reserveStreamFastPath, onInjectedHtml, onCleanup; RouterSsrLifecycle and serverSsrLifecycle option added; dehydrate now returns Awaitable.
Request Handler SSR Cleanup Tracking
packages/router-core/src/ssr/createRequestHandler.ts
Normalizes handler results to SsrResponse, computes responseOwnsCleanup from serverSsrCleanup === 'stream', returns normalized response, and defers/invokes cleanup accordingly.
SSR Server Attachment & Lifecycle Management
packages/router-core/src/ssr/ssr-server.ts
Refactors ScriptBuffer to injected callback model, listener-array lifecycle, finishScriptSerialization() coordination, reserveStreamFastPath(), and reentrancy-safe cleanup() with attach-time lifecycle hooks.
HTML Boundary & Closing-Tag Detection
packages/router-core/tests/closing-tag-detection.bench.ts
findHtmlBoundary and multiple detection variants implemented; verification and benchmarks added for </body> detection and boundary scans.
Stream Transformation with Backpressure & Abort
packages/router-core/src/ssr/transformStreamWithRouter.ts, packages/router-core/tests/transformStreamWithRouter.test.ts
Dual-path transform (fast passthrough vs main scanner/inject) with backpressure-aware pending-output queue, bounded router-html/tail/leftover buffers, TransformStreamWithRouterOptions including onAbort, serialization gating, timeouts, and extensive tests.
React SSR Streaming & Abort Handling
packages/react-router/src/ssr/renderRouterToStream.tsx, packages/react-router/tests/renderRouterToStream.test.tsx
Adds waitForReadyOrAbort for bots, robust pipeable/readable abort/error handling, passes onAbort to transform, returns createSsrStreamResponse; tests cover sync/async errors and aborts.
Solid SSR Streaming & Abort Handling
packages/solid-router/src/ssr/renderRouterToStream.tsx, packages/solid-router/tests/renderRouterToStream.test.tsx
Introduces inner-writer abort layer, waitForReadyOrAbort, solidWritable proxy for pipeTo, onAbort wiring, and SsrStreamResponse wrapping; tests for bot-abort and pipeTo rejection.
Vue SSR Streaming & Abort Handling
packages/vue-router/src/ssr/renderRouterToStream.tsx, packages/vue-router/tests/renderRouterToStream.test.tsx
Rewrites prependDoctype reader lifecycle; bot render-to-string raced with abort; streaming path uses TransformStream-backed writable proxy, abort listener cleanup, onAbort to transform, and SsrStreamResponse wrapping; tests validate marker placement, pipe errors, and aborts.
Start Middleware SSR Response Handling
packages/start-server-core/src/createStartHandler.ts, packages/start-server-core/tests/createStartHandler.test.ts
executeMiddleware normalized to return { ctx, response }, detects SsrResponse, centralizes response assignment and disposal, handleRedirectResponse/HEAD handling use replaceSsrResponse/stripSsrResponseBody; tests assert cleanup-ownership across middleware flows.
Query Cleanup & Stream Lifecycle
packages/router-ssr-query-core/src/index.ts, packages/router-ssr-query-core/package.json, packages/router-ssr-query-core/vite.config.ts
Adds idempotent teardown registered via serverSsrLifecycle.onServerSsrAttach to close streams/unsubscribe/cancel/clear; guards createPushableStream() terminal ops; package.json test script fixed; Vitest GC gating config added.
Query Integration Tests & GC Validation
packages/router-ssr-query-core/tests/index.test.ts
Refactored fixture for deferred SSR attach, tracked QueryClient cleanup, GC-gated WeakRef reclamation tests, and deterministic cleanup behavior tests.
Core SSR Streaming & Cleanup Tests
packages/router-core/tests/ssr-server-cleanup.test.ts, packages/router-core/tests/transformStreamWithRouter.test.ts, packages/router-core/tests/transformStreamBackpressure.perf.test.ts, packages/router-core/vite.config.ts
Extensive tests for cleanup contract, transform behavior, backpressure/perf harness (gated), and request-handler integration validating cleanup timing and memory safety.
E2E & Dist Artifacts
e2e/react-start/streaming-ssr/tests/query-heavy.spec.ts, e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/*
Adds e2e test asserting query stream payload order relative to SSR end marker; several built dist assets removed/trimmed in e2e fixture (bundled artifacts).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • Sheraff
  • SeanCassiere
  • beaussan

🐰 Streaming flows like carrots through fields,
Abort signals ring when cleanup yields,
Backpressure tucks buffers snug and tight,
Listener hooks make SSR lifecycle right. 🥕

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-streaming

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 28, 2026

View your CI Pipeline Execution ↗ for commit ef37315

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 8m 46s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-29 00:26:45 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

🚀 Changeset Version Preview

6 package(s) bumped directly, 21 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/react-router 1.170.8 → 1.170.9 Changeset
@tanstack/router-core 1.171.6 → 1.171.7 Changeset
@tanstack/router-ssr-query-core 1.169.0 → 1.169.1 Changeset
@tanstack/solid-router 1.170.8 → 1.170.9 Changeset
@tanstack/start-server-core 1.169.5 → 1.169.6 Changeset
@tanstack/vue-router 1.170.8 → 1.170.9 Changeset
@tanstack/react-router-ssr-query 1.167.0 → 1.167.1 Dependent
@tanstack/react-start 1.168.15 → 1.168.16 Dependent
@tanstack/react-start-client 1.168.5 → 1.168.6 Dependent
@tanstack/react-start-rsc 0.1.14 → 0.1.15 Dependent
@tanstack/react-start-server 1.167.10 → 1.167.11 Dependent
@tanstack/router-cli 1.167.10 → 1.167.11 Dependent
@tanstack/router-generator 1.167.10 → 1.167.11 Dependent
@tanstack/router-plugin 1.168.11 → 1.168.12 Dependent
@tanstack/router-vite-plugin 1.167.11 → 1.167.12 Dependent
@tanstack/solid-router-ssr-query 1.167.0 → 1.167.1 Dependent
@tanstack/solid-start 1.168.15 → 1.168.16 Dependent
@tanstack/solid-start-client 1.168.5 → 1.168.6 Dependent
@tanstack/solid-start-server 1.167.10 → 1.167.11 Dependent
@tanstack/start-client-core 1.170.4 → 1.170.5 Dependent
@tanstack/start-plugin-core 1.171.7 → 1.171.8 Dependent
@tanstack/start-static-server-functions 1.167.9 → 1.167.10 Dependent
@tanstack/start-storage-context 1.167.8 → 1.167.9 Dependent
@tanstack/vue-router-ssr-query 1.167.0 → 1.167.1 Dependent
@tanstack/vue-start 1.168.14 → 1.168.15 Dependent
@tanstack/vue-start-client 1.167.9 → 1.167.10 Dependent
@tanstack/vue-start-server 1.167.10 → 1.167.11 Dependent

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

Bundle Size Benchmarks

  • Commit: aa867fc2c431
  • Measured at: 2026-05-29T00:19:04.078Z
  • Baseline source: history:c79b3964121c
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.30 KiB 0 B (0.00%) 87.16 KiB 274.03 KiB 75.92 KiB ▁▁▁████████
react-router.full 90.75 KiB 0 B (0.00%) 90.61 KiB 285.39 KiB 78.80 KiB █▁▁▂▂▂▂▂▂▂▂
solid-router.minimal 35.53 KiB 0 B (0.00%) 35.41 KiB 106.33 KiB 32.01 KiB ▁▁▁████████
solid-router.full 40.23 KiB 0 B (0.00%) 40.11 KiB 120.52 KiB 36.19 KiB █▂▂▁▁▁▁▁▁▁▁
vue-router.minimal 53.02 KiB 0 B (0.00%) 52.89 KiB 150.35 KiB 47.56 KiB ███▁▁▁▁▁▁▁▁
vue-router.full 58.65 KiB 0 B (0.00%) 58.52 KiB 168.08 KiB 52.55 KiB ▁██▅▅▅▅▅▅▅▅
react-start.minimal 101.93 KiB 0 B (0.00%) 101.79 KiB 322.35 KiB 88.25 KiB █▁▁▁▁▁▁▁▁▁▁
react-start.deferred-hydration 102.66 KiB 0 B (0.00%) 101.81 KiB 323.72 KiB 88.82 KiB █▆▆▁▁▁▁▁▁▁▁
react-start.full 105.31 KiB 0 B (0.00%) 105.17 KiB 332.66 KiB 91.06 KiB █▁▁▁▁▁▁▁▁▁▁
react-start.rsbuild.minimal 99.61 KiB 0 B (0.00%) 99.44 KiB 316.79 KiB 85.73 KiB █▁▁▁▁▂▂▂▂▂▂
react-start.rsbuild.full 102.88 KiB 0 B (0.00%) 102.71 KiB 327.18 KiB 88.52 KiB █▁▁▁▁▁▁▁▁▁▁
solid-start.minimal 49.63 KiB 0 B (0.00%) 49.50 KiB 152.40 KiB 43.84 KiB █▁▁▁▁▁▁▁▁▁▁
solid-start.deferred-hydration 52.89 KiB 0 B (0.00%) 49.55 KiB 160.44 KiB 46.71 KiB ███▁▁▁▁▁▁▁▁
solid-start.full 55.41 KiB 0 B (0.00%) 55.29 KiB 169.33 KiB 48.81 KiB █▁▁▁▁▁▁▁▁▁▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7497

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7497

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7497

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7497

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7497

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7497

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7497

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7497

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7497

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7497

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7497

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7497

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7497

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7497

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7497

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7497

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7497

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7497

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7497

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7497

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7497

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7497

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7497

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7497

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7497

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7497

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7497

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7497

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7497

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7497

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7497

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7497

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7497

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7497

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7497

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7497

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7497

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7497

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7497

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7497

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7497

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7497

commit: 0c0a6cc

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
packages/vue-router/tests/renderRouterToStream.test.tsx (1)

40-52: 💤 Low value

Consider adding await router.serverSsr!.dehydrate() in buildRouter.

The bot test (line 70) manually calls dehydrate(), but buildRouter doesn't include it. For consistency with the pattern across all framework tests, consider including it in the helper or documenting why it's only needed for the bot test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue-router/tests/renderRouterToStream.test.tsx` around lines 40 -
52, The helper buildRouter sets up server SSR utils but does not call the
server-side dehydrate step, causing inconsistent setup vs tests that call
router.serverSsr!.dehydrate() manually; update buildRouter to await
router.serverSsr!.dehydrate() after attachRouterServerSsrUtils({ router,
manifest: undefined }) and before returning the router (or document in a comment
why a specific test should call dehydrate itself), so tests can rely on
buildRouter returning a fully dehydrated server router.
packages/react-router/tests/renderRouterToStream.test.tsx (1)

23-34: 💤 Low value

Consider adding await router.serverSsr!.dehydrate() for completeness.

The buildRouter helper loads the router but doesn't call dehydrate(). While some tests (like line 32 in the setup throw test's buildRouter call) do call it separately, other tests rely on buildRouter directly. This inconsistency could lead to flaky behavior if future tests expect dehydration to have occurred.

Looking at the Solid/Vue test files, they also have this pattern inconsistently applied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-router/tests/renderRouterToStream.test.tsx` around lines 23 -
34, The helper buildRouter does not consistently dehydrate the server SSR state;
update buildRouter to call and await router.serverSsr!.dehydrate() after
attachRouterServerSsrUtils({ router, manifest: undefined }) and after await
router.load(), so that the router is always dehydrated before return; locate the
function buildRouter and add the await router.serverSsr!.dehydrate() invocation
(using the existing router.serverSsr property) prior to returning the router.
packages/solid-router/tests/renderRouterToStream.test.tsx (1)

34-46: 💤 Low value

Consider adding await router.serverSsr!.dehydrate() for test consistency.

Similar to the React tests, buildRouter doesn't call dehydrate(). While this may work for the current tests, adding it would match the pattern in other test files and ensure the router is in the expected SSR state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-router/tests/renderRouterToStream.test.tsx` around lines 34 -
46, Add a call to dehydrate the server SSR state in buildRouter: after attaching
SSR utils with attachRouterServerSsrUtils({ router, manifest: undefined }) and
finishing router.load(), invoke await router.serverSsr!.dehydrate() so the
router is put into the same SSR-dehydrated state used by other tests; reference
the buildRouter function and the router.serverSsr!.dehydrate() method when
making this change.
packages/start-server-core/src/createStartHandler.ts (1)

214-217: 🏗️ Heavy lift

Replace TODO/any middleware contracts with concrete types.

executeMiddleware now sits on a critical SSR cleanup path, but still accepts/returns TODO (any) in core control flow. Tightening these types would prevent accidental invalid response/context shapes from bypassing cleanup logic.

As per coding guidelines **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

Also applies to: 524-532, 648-656

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-server-core/src/createStartHandler.ts` around lines 214 - 217,
The executeMiddleware signature (and analogous spots at the other occurrences)
uses TODO/any for middleware and ctx; replace these with concrete, strict types:
define a RequestContext (or reuse existing context type used across the SSR
pipeline) for ctx, a Middleware type like (ctx: RequestContext) => Promise<void
| HandlerCallbackResult | Response> (or synchronous equivalents), and ensure
executeMiddleware returns Promise<{ ctx: RequestContext; response:
HandlerCallbackResult }>; update any intermediate helper types (e.g.,
HandlerCallbackResult) so their shape is explicit (status, headers, body, etc.),
and propagate these concrete types into the other two locations referenced so
the cleanup path enforces correct shapes rather than any/ TODO.
packages/start-server-core/tests/createStartHandler.test.ts (1)

72-82: ⚡ Quick win

Reduce repeated as any casts in new SSR test helpers.

Please introduce small helper types (typed router/typed SSR response) so ownership tests keep compile-time guarantees without pervasive any casts.

As per coding guidelines **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

Also applies to: 106-107, 129-130, 189-190, 217-218, 251-252, 277-278, 304-305

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-server-core/tests/createStartHandler.test.ts` around lines 72
- 82, Replace the repeated `as any` casts by introducing small test-only types
and helper factories: add a TypedTestRouter (or TestRouter) interface
representing the minimal router shape used by attachRouterServerSsrUtils and
createSsrStreamResponse, and a helper function (e.g., makeTestRouter()) that
returns a strongly-typed instance; likewise add a TypedSsrResponse or
makeTestSsrResponse(stream) helper that returns Response with the proper typed
serverSsr attached. Update calls to attachRouterServerSsrUtils({ router: router
as any, ... }) and createSsrStreamResponse(router as any, ...) to use the new
test helpers or typed variables (router: TestRouter) so the code at symbols
attachRouterServerSsrUtils and createSsrStreamResponse no longer needs `as any`;
apply the same pattern to the other occurrences listed (lines ~106-107, 129-130,
189-190, 217-218, 251-252, 277-278, 304-305).
packages/router-core/tests/transformStreamWithRouter.test.ts (1)

185-199: 🏗️ Heavy lift

Consolidate typed test fakes to remove repeated as any casts.

The suite is strong, but repeated any casts weaken strict-mode guarantees. A small typed test harness (typed fake router + typed transform wrappers) would keep the same coverage without type escapes.

As per coding guidelines **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

Also applies to: 223-226, 946-950

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/transformStreamWithRouter.test.ts` around lines
185 - 199, Tests repeatedly use `as any` casts (weakening strict-mode
guarantees) around the test router and transform wrappers; update the test
harness to provide properly typed fakes instead of casting. Concretely, add a
typed helper for building SSR routers (refactor createRealSsrRouter to accept
and return precise generics) and create typed transform wrappers used in
transformStreamWithRouter.test so callers of BaseRootRoute, BaseRoute,
createTestRouter and createMemoryHistory no longer require `as any`; update
usages at the noted locations (including the other occurrences around lines
223-226 and 946-950) to use the new typed helpers and remove the `as any` casts.
Ensure the new helpers' signatures reflect the real route/component types so the
TypeScript compiler enforces strict-mode types across the tests.
packages/router-core/tests/ssr-server-cleanup.test.ts (1)

18-18: ⚡ Quick win

Reduce any usage in new test helpers/calls to keep strict typing coverage meaningful.

Please replace Record<string, any> and as any casts here with unknown/narrowed helper types (e.g., typed fake router/stream aliases). This keeps tests aligned with strict-mode guarantees.

As per coding guidelines **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

Also applies to: 364-370

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/ssr-server-cleanup.test.ts` at line 18, The test
helper buildRouter currently types its parameter as Record<string, any> and uses
as any casts; replace that broad any with a stricter type (e.g., unknown or a
narrow helper interface like DehydratedState | undefined) and update callers to
narrow/validate that data before use; also remove any "as any" casts in this
file (and the other occurrences around the 364-370 area) by introducing small
test-only types or typed fake router/stream aliases (e.g., FakeRouter,
FakeStream) and use type guards or explicit conversion helpers to map unknown to
those types so the tests remain strictly typed under TS strict mode while
preserving the same runtime behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/react-start/streaming-ssr/tests/query-heavy.spec.ts`:
- Around line 144-145: The test currently checks for the broad substring
'slow-async' which can match static markup; update the assertions in
query-heavy.spec.ts to assert against a concrete payload-specific token from the
stream (e.g., the exact streamed value token emitted for the slow async query)
instead of the generic 'slow-async' substring; specifically, change the two
expectations that use html.indexOf('slow-async') to search for the exact payload
token string (still using html and endIndex) and assert that its index is >= 0
and < endIndex so the streamed payload ordering is validated precisely.

In `@packages/router-core/src/ssr/handlerCallback.ts`:
- Around line 45-57: The dispose function currently returns immediately after a
successful await on response.body!.cancel(reason), which prevents
router.serverSsr?.cleanup() from running; modify dispose (the async dispose
method that checks disposed) to always invoke router.serverSsr?.cleanup() after
attempting response.body!.cancel(reason) — e.g., remove the premature return
and/or use try/finally so that regardless of successful cancel or thrown errors,
router.serverSsr?.cleanup() is executed (keep the disposed guard as-is).

In `@packages/router-core/src/ssr/transformStreamWithRouter.ts`:
- Around line 21-24: Update the transformReadableStreamWithRouter signature to
type routerStream as ReadableStream<Uint8Array | string> (and adjust any other
file functions that currently use untyped ReadableStream similarly), update
related types such as TransformStreamWithRouterOptions to reflect the stricter
union where used, and remove the ArrayBufferView cast-paths inside the
implementation; instead branch on chunk type (string vs Uint8Array) and handle
conversions explicitly (e.g., TextEncoder/decoder or direct passthrough) so no
unsafe ArrayBufferView casts remain and TypeScript strict mode is satisfied.

In `@packages/router-core/tests/closing-tag-detection.bench.ts`:
- Around line 266-269: The function findBodyEndTagNativeLowerThenLastOpenSlash
currently searches for the literal lowercase BODY_END_TAG in the original string
and skips the fallback, causing mixed-case tags like </BODY> to be missed;
change it to perform a case-insensitive lastIndexOf by searching on a lowercased
copy of the input (e.g., const lower = str.toLowerCase(); const index =
lower.lastIndexOf(BODY_END_TAG)) and then return index === -1 ?
findBodyEndTagLastOpenSlash(str) : index so the index stays valid and mixed-case
tags are found.

In `@packages/start-server-core/src/createStartHandler.ts`:
- Around line 223-233: The setResponse helper currently overwrites the
module-scoped streamResponse when an SsrResponse with serverSsrCleanup ===
'stream' arrives, leaking the previous stream; make setResponse async, check if
streamResponse exists and call its disposal/cleanup method (or await any
provided cleanup promise) before assigning the new stream, retain the
isSsrResponse and serverSsrCleanup checks, and update all call sites to await
setResponse(...) so disposal completes before continuing.

---

Nitpick comments:
In `@packages/react-router/tests/renderRouterToStream.test.tsx`:
- Around line 23-34: The helper buildRouter does not consistently dehydrate the
server SSR state; update buildRouter to call and await
router.serverSsr!.dehydrate() after attachRouterServerSsrUtils({ router,
manifest: undefined }) and after await router.load(), so that the router is
always dehydrated before return; locate the function buildRouter and add the
await router.serverSsr!.dehydrate() invocation (using the existing
router.serverSsr property) prior to returning the router.

In `@packages/router-core/tests/ssr-server-cleanup.test.ts`:
- Line 18: The test helper buildRouter currently types its parameter as
Record<string, any> and uses as any casts; replace that broad any with a
stricter type (e.g., unknown or a narrow helper interface like DehydratedState |
undefined) and update callers to narrow/validate that data before use; also
remove any "as any" casts in this file (and the other occurrences around the
364-370 area) by introducing small test-only types or typed fake router/stream
aliases (e.g., FakeRouter, FakeStream) and use type guards or explicit
conversion helpers to map unknown to those types so the tests remain strictly
typed under TS strict mode while preserving the same runtime behavior.

In `@packages/router-core/tests/transformStreamWithRouter.test.ts`:
- Around line 185-199: Tests repeatedly use `as any` casts (weakening
strict-mode guarantees) around the test router and transform wrappers; update
the test harness to provide properly typed fakes instead of casting. Concretely,
add a typed helper for building SSR routers (refactor createRealSsrRouter to
accept and return precise generics) and create typed transform wrappers used in
transformStreamWithRouter.test so callers of BaseRootRoute, BaseRoute,
createTestRouter and createMemoryHistory no longer require `as any`; update
usages at the noted locations (including the other occurrences around lines
223-226 and 946-950) to use the new typed helpers and remove the `as any` casts.
Ensure the new helpers' signatures reflect the real route/component types so the
TypeScript compiler enforces strict-mode types across the tests.

In `@packages/solid-router/tests/renderRouterToStream.test.tsx`:
- Around line 34-46: Add a call to dehydrate the server SSR state in
buildRouter: after attaching SSR utils with attachRouterServerSsrUtils({ router,
manifest: undefined }) and finishing router.load(), invoke await
router.serverSsr!.dehydrate() so the router is put into the same SSR-dehydrated
state used by other tests; reference the buildRouter function and the
router.serverSsr!.dehydrate() method when making this change.

In `@packages/start-server-core/src/createStartHandler.ts`:
- Around line 214-217: The executeMiddleware signature (and analogous spots at
the other occurrences) uses TODO/any for middleware and ctx; replace these with
concrete, strict types: define a RequestContext (or reuse existing context type
used across the SSR pipeline) for ctx, a Middleware type like (ctx:
RequestContext) => Promise<void | HandlerCallbackResult | Response> (or
synchronous equivalents), and ensure executeMiddleware returns Promise<{ ctx:
RequestContext; response: HandlerCallbackResult }>; update any intermediate
helper types (e.g., HandlerCallbackResult) so their shape is explicit (status,
headers, body, etc.), and propagate these concrete types into the other two
locations referenced so the cleanup path enforces correct shapes rather than
any/ TODO.

In `@packages/start-server-core/tests/createStartHandler.test.ts`:
- Around line 72-82: Replace the repeated `as any` casts by introducing small
test-only types and helper factories: add a TypedTestRouter (or TestRouter)
interface representing the minimal router shape used by
attachRouterServerSsrUtils and createSsrStreamResponse, and a helper function
(e.g., makeTestRouter()) that returns a strongly-typed instance; likewise add a
TypedSsrResponse or makeTestSsrResponse(stream) helper that returns Response
with the proper typed serverSsr attached. Update calls to
attachRouterServerSsrUtils({ router: router as any, ... }) and
createSsrStreamResponse(router as any, ...) to use the new test helpers or typed
variables (router: TestRouter) so the code at symbols attachRouterServerSsrUtils
and createSsrStreamResponse no longer needs `as any`; apply the same pattern to
the other occurrences listed (lines ~106-107, 129-130, 189-190, 217-218,
251-252, 277-278, 304-305).

In `@packages/vue-router/tests/renderRouterToStream.test.tsx`:
- Around line 40-52: The helper buildRouter sets up server SSR utils but does
not call the server-side dehydrate step, causing inconsistent setup vs tests
that call router.serverSsr!.dehydrate() manually; update buildRouter to await
router.serverSsr!.dehydrate() after attachRouterServerSsrUtils({ router,
manifest: undefined }) and before returning the router (or document in a comment
why a specific test should call dehydrate itself), so tests can rely on
buildRouter returning a fully dehydrated server router.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 252d919e-92b8-4f8b-b32d-6675ad3b362e

📥 Commits

Reviewing files that changed from the base of the PR and between bae50be and f680d15.

📒 Files selected for processing (37)
  • .changeset/fuzzy-jobs-eat.md
  • e2e/react-start/streaming-ssr/tests/query-heavy.spec.ts
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/index-BwsVWHwV.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/index-ElfLiipA.css
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/index-rPfEOKs3.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/lazy-page.lazy-BzoMuhUY.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/lazy-with-loader-page.lazy-BzLmn584.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/normal-page-BPOVyYTF.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/page-with-search-BhJv4yJI.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/scroll-block-BAas_Ct-.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/virtual-page.lazy-BCqvdivF.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/index.html
  • packages/react-router/src/ssr/renderRouterToStream.tsx
  • packages/react-router/tests/renderRouterToStream.test.tsx
  • packages/router-core/src/router.ts
  • packages/router-core/src/ssr/createRequestHandler.ts
  • packages/router-core/src/ssr/handlerCallback.ts
  • packages/router-core/src/ssr/server.ts
  • packages/router-core/src/ssr/ssr-server.ts
  • packages/router-core/src/ssr/transformStreamWithRouter.ts
  • packages/router-core/tests/closing-tag-detection.bench.ts
  • packages/router-core/tests/ssr-server-cleanup.test.ts
  • packages/router-core/tests/transformStreamBackpressure.perf.test.ts
  • packages/router-core/tests/transformStreamWithRouter.test.ts
  • packages/router-core/vite.config.ts
  • packages/router-ssr-query-core/package.json
  • packages/router-ssr-query-core/src/index.ts
  • packages/router-ssr-query-core/tests/index.test.ts
  • packages/router-ssr-query-core/vite.config.ts
  • packages/solid-router/src/ssr/renderRouterToStream.tsx
  • packages/solid-router/tests/renderRouterToStream.test.tsx
  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-server-core/tests/createStartHandler.test.ts
  • packages/start-server-core/tests/fixtures/start-manifest.ts
  • packages/start-server-core/vite.config.ts
  • packages/vue-router/src/ssr/renderRouterToStream.tsx
  • packages/vue-router/tests/renderRouterToStream.test.tsx
💤 Files with no reviewable changes (9)
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/normal-page-BPOVyYTF.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/page-with-search-BhJv4yJI.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/lazy-with-loader-page.lazy-BzLmn584.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/lazy-page.lazy-BzoMuhUY.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/index-ElfLiipA.css
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/virtual-page.lazy-BCqvdivF.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/index-rPfEOKs3.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/assets/scroll-block-BAas_Ct-.js
  • e2e/vue-router/scroll-restoration-sandbox-vite/dist-hash/index.html

Comment thread e2e/react-start/streaming-ssr/tests/query-heavy.spec.ts Outdated
Comment thread packages/router-core/src/ssr/handlerCallback.ts
Comment on lines 21 to +24
export function transformReadableStreamWithRouter(
router: AnyRouter,
routerStream: ReadableStream,
opts?: TransformStreamWithRouterOptions,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Tighten stream chunk typing to avoid unsafe casts in the core transform path.

These signatures use untyped ReadableStream, which forces downstream coercion. Please type the stream as ReadableStream<Uint8Array | string> (or the exact supported union) and remove the ArrayBufferView cast path where possible.

As per coding guidelines **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

Also applies to: 199-203, 781-785

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/ssr/transformStreamWithRouter.ts` around lines 21 -
24, Update the transformReadableStreamWithRouter signature to type routerStream
as ReadableStream<Uint8Array | string> (and adjust any other file functions that
currently use untyped ReadableStream similarly), update related types such as
TransformStreamWithRouterOptions to reflect the stricter union where used, and
remove the ArrayBufferView cast-paths inside the implementation; instead branch
on chunk type (string vs Uint8Array) and handle conversions explicitly (e.g.,
TextEncoder/decoder or direct passthrough) so no unsafe ArrayBufferView casts
remain and TypeScript strict mode is satisfied.

Comment on lines +266 to +269
function findBodyEndTagNativeLowerThenLastOpenSlash(str: string): number {
const index = str.lastIndexOf(BODY_END_TAG)
return index === -1 ? findBodyEndTagLastOpenSlash(str) : index
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Case-insensitive “last </body>” detection is incorrect in mixed-case input.

This shortcut returns the last lowercase </body> and skips fallback, so a later uppercase </BODY> is missed.

Suggested fix
 function findBodyEndTagNativeLowerThenLastOpenSlash(str: string): number {
-  const index = str.lastIndexOf(BODY_END_TAG)
-  return index === -1 ? findBodyEndTagLastOpenSlash(str) : index
+  // Keep this variant correct for mixed-case tails; "native lowercase first"
+  // can miss a later uppercase </BODY>.
+  return findBodyEndTagLastOpenSlash(str)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/closing-tag-detection.bench.ts` around lines 266 -
269, The function findBodyEndTagNativeLowerThenLastOpenSlash currently searches
for the literal lowercase BODY_END_TAG in the original string and skips the
fallback, causing mixed-case tags like </BODY> to be missed; change it to
perform a case-insensitive lastIndexOf by searching on a lowercased copy of the
input (e.g., const lower = str.toLowerCase(); const index =
lower.lastIndexOf(BODY_END_TAG)) and then return index === -1 ?
findBodyEndTagLastOpenSlash(str) : index so the index stays valid and mixed-case
tags are found.

Comment on lines +223 to +233
const setResponse = (response: TODO) => {
if (isSsrResponse(response)) {
if (response.serverSsrCleanup === 'stream') {
streamResponse = response
}
ctx.response = response.response
return
}

ctx.response = response
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Dispose previously tracked stream before overwriting streamResponse.

When a second streaming SsrResponse is assigned, the previous tracked stream handle is overwritten without disposal. That can leak SSR cleanup ownership for the earlier stream in the same middleware chain.

Proposed direction
-  const setResponse = (response: TODO) => {
+  const setResponse = async (response: TODO) => {
     if (isSsrResponse(response)) {
       if (response.serverSsrCleanup === 'stream') {
+        if (streamResponse && streamResponse !== response) {
+          await streamResponse.dispose('middleware stream replaced')
+        }
         streamResponse = response
       }
       ctx.response = response.response
       return
     }

     ctx.response = response
   }

Then await setResponse(...) at call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-server-core/src/createStartHandler.ts` around lines 223 - 233,
The setResponse helper currently overwrites the module-scoped streamResponse
when an SsrResponse with serverSsrCleanup === 'stream' arrives, leaking the
previous stream; make setResponse async, check if streamResponse exists and call
its disposal/cleanup method (or await any provided cleanup promise) before
assigning the new stream, retain the isSsrResponse and serverSsrCleanup checks,
and update all call sites to await setResponse(...) so disposal completes before
continuing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there can only be a single SSR response per request

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schiller-manuel, understood — since the architecture guarantees at most one SSR response per request, the overwrite scenario I was worried about can't occur. My concern doesn't apply here. Thanks for the clarification!

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 28, 2026

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing fix-streaming (0c0a6cc) with main (c79b396)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@schiller-manuel schiller-manuel merged commit d1997b6 into main May 29, 2026
19 checks passed
@schiller-manuel schiller-manuel deleted the fix-streaming branch May 29, 2026 00:27
@github-actions github-actions Bot mentioned this pull request May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Start] SSR memory leak under sustained load — two retention paths (queryClient gcTime + native stream buffers)

1 participant