diff --git a/.changeset/quiet-cloudflare-streams.md b/.changeset/quiet-cloudflare-streams.md
new file mode 100644
index 00000000000..2378f168b70
--- /dev/null
+++ b/.changeset/quiet-cloudflare-streams.md
@@ -0,0 +1,6 @@
+---
+'@tanstack/router-core': patch
+'@tanstack/start-server-core': patch
+---
+
+Keep shared SSR stream transforms on Web Streams so Solid Start can use Cloudflare's narrow `nodejs_als` compatibility flag. Remove the unused Node pipeable adapter exports; React keeps its existing Node conversion at the renderer boundary.
diff --git a/e2e/solid-start/basic-cloudflare/src/routes/index.tsx b/e2e/solid-start/basic-cloudflare/src/routes/index.tsx
index 76e285a2baf..d3f3d7291b6 100644
--- a/e2e/solid-start/basic-cloudflare/src/routes/index.tsx
+++ b/e2e/solid-start/basic-cloudflare/src/routes/index.tsx
@@ -1,5 +1,6 @@
import { createFileRoute } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
+import { getRequestHeader } from '@tanstack/solid-start/server'
import { env } from 'cloudflare:workers'
export const Route = createFileRoute('/')({
@@ -11,6 +12,7 @@ const getData = createServerFn().handler(() => {
return {
message: `Running in ${navigator.userAgent}`,
myVar: env.MY_VAR,
+ requestHeader: getRequestHeader('x-solid-start-context'),
}
})
@@ -22,6 +24,7 @@ function Home() {
Welcome Home!!!
{data().message}
{data().myVar}
+ {data().requestHeader}
)
}
diff --git a/e2e/solid-start/basic-cloudflare/tests/app.spec.ts b/e2e/solid-start/basic-cloudflare/tests/app.spec.ts
index add28cb73ee..6160dabe11a 100644
--- a/e2e/solid-start/basic-cloudflare/tests/app.spec.ts
+++ b/e2e/solid-start/basic-cloudflare/tests/app.spec.ts
@@ -17,6 +17,16 @@ test('returns the correct value from a Cloudflare binding', async ({
await expect(page.getByTestId('myVar')).toHaveText('Hello from Cloudflare')
})
+test('preserves request context with nodejs_als', async ({ page }) => {
+ await page.setExtraHTTPHeaders({
+ 'x-solid-start-context': 'request-context-works',
+ })
+ await page.goto('/')
+ await expect(page.getByTestId('requestHeader')).toHaveText(
+ 'request-context-works',
+ )
+})
+
test('prerender with Cloudflare Workers runtime', async ({ page }) => {
// Verify the static page was prerendered during build
const distDir = join(process.cwd(), 'dist', 'client')
diff --git a/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts b/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts
index f7a8f163972..be4bba60c37 100644
--- a/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts
+++ b/e2e/solid-start/basic-cloudflare/worker-configuration.d.ts
@@ -1,18 +1,12 @@
/* eslint-disable */
-// Generated by Wrangler by running `wrangler types` (hash: b11df627d8b3c51b1bf3230a546b0f20)
-// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_compat
+// Generated by Wrangler by running `wrangler types` (hash: 24a3fe972e2c4aaa57bb30eaba75ee7a)
+// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_als
declare namespace Cloudflare {
interface Env {
MY_VAR: "Hello from Cloudflare";
}
}
interface Env extends Cloudflare.Env {}
-type StringifyValues> = {
- [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
-};
-declare namespace NodeJS {
- interface ProcessEnv extends StringifyValues> {}
-}
// Begin runtime types
/*! *****************************************************************************
diff --git a/e2e/solid-start/basic-cloudflare/wrangler.jsonc b/e2e/solid-start/basic-cloudflare/wrangler.jsonc
index 833d63d85a6..d218706c38b 100644
--- a/e2e/solid-start/basic-cloudflare/wrangler.jsonc
+++ b/e2e/solid-start/basic-cloudflare/wrangler.jsonc
@@ -2,7 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json",
"name": "tanstack-start-app",
"compatibility_date": "2025-09-24",
- "compatibility_flags": ["nodejs_compat"],
+ "compatibility_flags": ["nodejs_als"],
"main": "@tanstack/solid-start/server-entry",
"vars": {
"MY_VAR": "Hello from Cloudflare",
diff --git a/examples/solid/start-basic-cloudflare/worker-configuration.d.ts b/examples/solid/start-basic-cloudflare/worker-configuration.d.ts
index f7a8f163972..be4bba60c37 100644
--- a/examples/solid/start-basic-cloudflare/worker-configuration.d.ts
+++ b/examples/solid/start-basic-cloudflare/worker-configuration.d.ts
@@ -1,18 +1,12 @@
/* eslint-disable */
-// Generated by Wrangler by running `wrangler types` (hash: b11df627d8b3c51b1bf3230a546b0f20)
-// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_compat
+// Generated by Wrangler by running `wrangler types` (hash: 24a3fe972e2c4aaa57bb30eaba75ee7a)
+// Runtime types generated with workerd@1.20260317.1 2025-09-24 nodejs_als
declare namespace Cloudflare {
interface Env {
MY_VAR: "Hello from Cloudflare";
}
}
interface Env extends Cloudflare.Env {}
-type StringifyValues> = {
- [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
-};
-declare namespace NodeJS {
- interface ProcessEnv extends StringifyValues> {}
-}
// Begin runtime types
/*! *****************************************************************************
diff --git a/examples/solid/start-basic-cloudflare/wrangler.jsonc b/examples/solid/start-basic-cloudflare/wrangler.jsonc
index 833d63d85a6..d218706c38b 100644
--- a/examples/solid/start-basic-cloudflare/wrangler.jsonc
+++ b/examples/solid/start-basic-cloudflare/wrangler.jsonc
@@ -2,7 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json",
"name": "tanstack-start-app",
"compatibility_date": "2025-09-24",
- "compatibility_flags": ["nodejs_compat"],
+ "compatibility_flags": ["nodejs_als"],
"main": "@tanstack/solid-start/server-entry",
"vars": {
"MY_VAR": "Hello from Cloudflare",
diff --git a/packages/react-router/tests/renderRouterToStream.test.tsx b/packages/react-router/tests/renderRouterToStream.test.tsx
index cde31d0b6a9..ae19febb3a4 100644
--- a/packages/react-router/tests/renderRouterToStream.test.tsx
+++ b/packages/react-router/tests/renderRouterToStream.test.tsx
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest'
import { attachRouterServerSsrUtils } from '@tanstack/router-core/ssr/server'
import { createMemoryHistory, createRootRoute, createRouter } from '../src'
+import type { PassThrough } from 'node:stream'
import type { RouterManagedTag } from '@tanstack/router-core'
import type * as RouterSsrServer from '@tanstack/router-core/ssr/server'
import type * as ReactDomServer from 'react-dom/server'
@@ -742,6 +743,7 @@ describe('renderRouterToStream - renderer selection and pipeable errors', () =>
let options:
| Parameters[1]
| undefined
+ let destination: PassThrough | undefined
const abort = vi.fn((reason: unknown) => {
options?.onError?.(reason, { componentStack: '' })
})
@@ -749,11 +751,18 @@ describe('renderRouterToStream - renderer selection and pipeable errors', () =>
(_children, nextOptions) => {
options = nextOptions
queueMicrotask(() => nextOptions.onShellReady())
- return { abort, pipe: vi.fn() }
+ return {
+ abort,
+ pipe: vi.fn((stream: PassThrough) => {
+ destination = stream
+ return stream
+ }),
+ }
},
)
const router = await buildRouter()
+ const cleanup = vi.spyOn(router.serverSsr!, 'cleanup')
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const reason = new Error('pipeable-consumer-gone')
try {
@@ -767,7 +776,9 @@ describe('renderRouterToStream - renderer selection and pipeable errors', () =>
)
await response.body!.cancel(reason)
- expect(abort).toHaveBeenCalledWith(reason)
+ expect(abort).toHaveBeenCalledExactlyOnceWith(reason)
+ await vi.waitFor(() => expect(destination?.destroyed).toBe(true))
+ expect(cleanup).toHaveBeenCalledOnce()
expect(errorSpy).not.toHaveBeenCalled()
} finally {
errorSpy.mockRestore()
diff --git a/packages/router-core/src/ssr/server.ts b/packages/router-core/src/ssr/server.ts
index 84c34976c98..c0d2fd8be93 100644
--- a/packages/router-core/src/ssr/server.ts
+++ b/packages/router-core/src/ssr/server.ts
@@ -17,7 +17,6 @@ export type {
SsrResponse,
} from './handlerCallback'
export {
- transformPipeableStreamWithRouter,
transformHtmlStringWithRouter,
transformReadableStreamWithRouter,
} from './transformStreamWithRouter'
diff --git a/packages/router-core/src/ssr/transformStreamWithRouter.ts b/packages/router-core/src/ssr/transformStreamWithRouter.ts
index d253034073b..3c82bb6b47c 100644
--- a/packages/router-core/src/ssr/transformStreamWithRouter.ts
+++ b/packages/router-core/src/ssr/transformStreamWithRouter.ts
@@ -1,4 +1,3 @@
-import { Readable } from 'node:stream'
import {
DOCUMENT_CLOSE_ANCHOR_INDEX,
DOCUMENT_CLOSE_BYTES,
@@ -49,20 +48,6 @@ type AppStream =
| ReadableStream
| NodeReadableStream
-export function transformPipeableStreamWithRouter(
- router: AnyRouter,
- routerStream: Readable,
- opts?: TransformStreamWithRouterOptions,
-) {
- return Readable.fromWeb(
- transformReadableStreamWithRouter(
- router,
- Readable.toWeb(routerStream) as AppStream,
- opts,
- ) as NodeReadableStream,
- )
-}
-
export async function transformHtmlStringWithRouter(
router: AnyRouter,
html: string,
diff --git a/packages/router-core/tests/transformStreamWithRouter.test.ts b/packages/router-core/tests/transformStreamWithRouter.test.ts
index e5ce1d8d50d..056ff71f908 100644
--- a/packages/router-core/tests/transformStreamWithRouter.test.ts
+++ b/packages/router-core/tests/transformStreamWithRouter.test.ts
@@ -21,7 +21,6 @@ import {
import { attachRouterServerSsrUtils } from '../src/ssr/ssr-server'
import {
transformHtmlStringWithRouter,
- transformPipeableStreamWithRouter,
transformReadableStreamWithRouter,
} from '../src/ssr/transformStreamWithRouter'
import {
@@ -29,9 +28,27 @@ import {
SCRIPT_CLOSE,
} from '../src/ssr/htmlBoundaryScanner'
import { createTestRouter } from './routerTestUtils'
+import type { TransformStreamWithRouterOptions } from '../src/ssr/transformStreamWithRouter'
+import type { AnyRouter } from '../src/router'
+import type { ReadableStream as NodeReadableStream } from 'node:stream/web'
import type { RouterManagedTag } from '../src/manifest'
import type { HydrationScriptOutput } from '../src/ssr/hydrationScripts'
+// Node interoperability belongs at the caller boundary, not in the shared SSR module.
+function transformPipeableStreamWithRouter(
+ router: AnyRouter,
+ stream: Readable,
+ opts?: TransformStreamWithRouterOptions,
+) {
+ return Readable.fromWeb(
+ transformReadableStreamWithRouter(
+ router,
+ Readable.toWeb(stream) as ReadableStream,
+ opts,
+ ) as NodeReadableStream,
+ )
+}
+
const decoder = new TextDecoder()
const DOCUMENT_CLOSE = decoder.decode(DOCUMENT_CLOSE_BYTES)
const HYDRATION_SCRIPT_BOUNDARY_SUFFIX = decoder.decode(
diff --git a/packages/start-server-core/src/index.tsx b/packages/start-server-core/src/index.tsx
index 92937ff0392..3dbf00cbf1d 100644
--- a/packages/start-server-core/src/index.tsx
+++ b/packages/start-server-core/src/index.tsx
@@ -21,7 +21,6 @@ export {
createRequestHandler,
defineHandlerCallback,
transformReadableStreamWithRouter,
- transformPipeableStreamWithRouter,
} from '@tanstack/router-core/ssr/server'
export type { HandlerCallback } from '@tanstack/router-core/ssr/server'