Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions e2e/react-start/server-functions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { Route as RedirectTestSsrTargetRouteImport } from './routes/redirect-tes
import { Route as MiddlewareServerImportMiddlewareRouteImport } from './routes/middleware/server-import-middleware'
import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn'
import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware'
import { Route as MiddlewareMiddlewareFactoryRouteImport } from './routes/middleware/middleware-factory'
import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router'
import { Route as CookiesSetRouteImport } from './routes/cookies/set'
import { Route as AbortSignalMethodRouteImport } from './routes/abort-signal/$method'
Expand Down Expand Up @@ -184,6 +185,12 @@ const MiddlewareRequestMiddlewareRoute =
path: '/middleware/request-middleware',
getParentRoute: () => rootRouteImport,
} as any)
const MiddlewareMiddlewareFactoryRoute =
MiddlewareMiddlewareFactoryRouteImport.update({
id: '/middleware/middleware-factory',
path: '/middleware/middleware-factory',
getParentRoute: () => rootRouteImport,
} as any)
const MiddlewareClientMiddlewareRouterRoute =
MiddlewareClientMiddlewareRouterRouteImport.update({
id: '/middleware/client-middleware-router',
Expand Down Expand Up @@ -226,6 +233,7 @@ export interface FileRoutesByFullPath {
'/abort-signal/$method': typeof AbortSignalMethodRoute
'/cookies/set': typeof CookiesSetRoute
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
'/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute
Expand Down Expand Up @@ -260,6 +268,7 @@ export interface FileRoutesByTo {
'/abort-signal/$method': typeof AbortSignalMethodRoute
'/cookies/set': typeof CookiesSetRoute
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
'/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute
Expand Down Expand Up @@ -295,6 +304,7 @@ export interface FileRoutesById {
'/abort-signal/$method': typeof AbortSignalMethodRoute
'/cookies/set': typeof CookiesSetRoute
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
'/middleware/server-import-middleware': typeof MiddlewareServerImportMiddlewareRoute
Expand Down Expand Up @@ -331,6 +341,7 @@ export interface FileRouteTypes {
| '/abort-signal/$method'
| '/cookies/set'
| '/middleware/client-middleware-router'
| '/middleware/middleware-factory'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
| '/middleware/server-import-middleware'
Expand Down Expand Up @@ -365,6 +376,7 @@ export interface FileRouteTypes {
| '/abort-signal/$method'
| '/cookies/set'
| '/middleware/client-middleware-router'
| '/middleware/middleware-factory'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
| '/middleware/server-import-middleware'
Expand Down Expand Up @@ -399,6 +411,7 @@ export interface FileRouteTypes {
| '/abort-signal/$method'
| '/cookies/set'
| '/middleware/client-middleware-router'
| '/middleware/middleware-factory'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
| '/middleware/server-import-middleware'
Expand Down Expand Up @@ -434,6 +447,7 @@ export interface RootRouteChildren {
AbortSignalMethodRoute: typeof AbortSignalMethodRoute
CookiesSetRoute: typeof CookiesSetRoute
MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
MiddlewareMiddlewareFactoryRoute: typeof MiddlewareMiddlewareFactoryRoute
MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute
MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute
MiddlewareServerImportMiddlewareRoute: typeof MiddlewareServerImportMiddlewareRoute
Expand Down Expand Up @@ -648,6 +662,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MiddlewareRequestMiddlewareRouteImport
parentRoute: typeof rootRouteImport
}
'/middleware/middleware-factory': {
id: '/middleware/middleware-factory'
path: '/middleware/middleware-factory'
fullPath: '/middleware/middleware-factory'
preLoaderRoute: typeof MiddlewareMiddlewareFactoryRouteImport
parentRoute: typeof rootRouteImport
}
'/middleware/client-middleware-router': {
id: '/middleware/client-middleware-router'
path: '/middleware/client-middleware-router'
Expand Down Expand Up @@ -698,6 +719,7 @@ const rootRouteChildren: RootRouteChildren = {
AbortSignalMethodRoute: AbortSignalMethodRoute,
CookiesSetRoute: CookiesSetRoute,
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
MiddlewareMiddlewareFactoryRoute: MiddlewareMiddlewareFactoryRoute,
MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute,
MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
MiddlewareServerImportMiddlewareRoute: MiddlewareServerImportMiddlewareRoute,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ function RouteComponent() {
Server imports in middleware are stripped from client build
</Route.Link>
</li>
<li>
<Route.Link
to="./middleware-factory"
data-testid="middleware-factory-link"
>
Middleware factories with server imports are stripped from client
build
</Route.Link>
</li>
</ul>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { createFileRoute } from '@tanstack/react-router'
import { createMiddleware, createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import React from 'react'

/**
* This test verifies that middleware factories (functions that return createMiddleware().server())
* have their server-only code properly stripped from the client bundle.
*
* If the .server() part inside the factory is not stripped from the client build, this will fail with:
* "Module node:async_hooks has been externalized for browser compatibility"
* because @tanstack/react-start/server uses node:async_hooks internally.
*/

// Middleware factory function - returns a middleware with .server() call
function createHeaderMiddleware(headerName: string) {
return createMiddleware({ type: 'function' }).server(async ({ next }) => {
// Use a server-only import - this should be stripped from client build
const headers = getRequestHeaders()
const headerValue = headers.get(headerName) ?? 'missing'

console.log(`[middleware-factory] ${headerName}:`, headerValue)

return next({
context: {
headerName,
headerValue,
},
})
})
}

// Arrow function factory variant
const createPrefixedHeaderMiddleware = (prefix: string) => {
return createMiddleware({ type: 'function' }).server(async ({ next }) => {
// Use a server-only import - this should be stripped from client build
const headers = getRequestHeaders()
const allHeaderNames = [...headers.keys()]
const prefixedHeaders = allHeaderNames.filter((name) =>
name.toLowerCase().startsWith(prefix.toLowerCase()),
)

console.log(
`[middleware-factory] Prefixed headers (${prefix}):`,
prefixedHeaders,
)

return next({
context: {
prefix,
matchedHeaders: prefixedHeaders,
},
})
})
}

// Create middleware instances using the factories
const customHeaderMiddleware = createHeaderMiddleware('x-custom-factory-header')
const prefixedMiddleware = createPrefixedHeaderMiddleware('x-factory-')

const serverFn = createServerFn()
.middleware([customHeaderMiddleware, prefixedMiddleware])
.handler(async ({ context }) => {
return {
headerName: context.headerName,
headerValue: context.headerValue,
prefix: context.prefix,
matchedHeaders: context.matchedHeaders,
}
})

export const Route = createFileRoute('/middleware/middleware-factory')({
component: RouteComponent,
})

function RouteComponent() {
const [result, setResult] = React.useState<{
headerName: string
headerValue: string
prefix: string
matchedHeaders: Array<string>
} | null>(null)
const [error, setError] = React.useState<string | null>(null)

async function handleClick() {
try {
const data = await serverFn({
headers: {
'x-custom-factory-header': 'factory-header-value',
'x-factory-one': 'one',
'x-factory-two': 'two',
},
})
setResult(data)
setError(null)
} catch (e) {
setResult(null)
setError(e instanceof Error ? e.message : String(e))
}
}

return (
<div>
<h2>Middleware Factory Test</h2>
<p>
This test verifies that middleware factories (functions returning
createMiddleware().server()) have their server-only code properly
stripped from the client build.
</p>
<button onClick={handleClick} data-testid="test-middleware-factory-btn">
Call server function with factory middlewares
</button>
{result && (
<div data-testid="middleware-factory-result">
<div data-testid="header-value">{result.headerValue}</div>
<div data-testid="matched-headers">
{result.matchedHeaders.join(',')}
</div>
</div>
)}
{error && (
<div data-testid="middleware-factory-error">Error: {error}</div>
)}
</div>
)
}
29 changes: 29 additions & 0 deletions e2e/react-start/server-functions/tests/server-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,32 @@ test('server-only imports in middleware.server() are stripped from client build'
page.getByTestId('server-import-middleware-result'),
).toContainText('test-header-value')
})

test('middleware factories with server-only imports are stripped from client build', async ({
page,
}) => {
// This test verifies that middleware factories (functions returning createMiddleware().server())
// with server-only imports are properly stripped from the client build.
// If the .server() part inside the factory is not removed, the build would fail with
// node:async_hooks externalization errors because getRequestHeaders uses node:async_hooks internally.
// The fact that this page loads at all proves the server code was stripped correctly.
await page.goto('/middleware/middleware-factory')

await page.waitForLoadState('networkidle')

// Click the button to call the server function with factory middlewares
await page.getByTestId('test-middleware-factory-btn').click()

// Wait for the result - should contain our custom header value from the factory middleware
await expect(page.getByTestId('header-value')).toContainText(
'factory-header-value',
)

// Also verify the prefixed headers were matched correctly
await expect(page.getByTestId('matched-headers')).toContainText(
'x-factory-one',
)
await expect(page.getByTestId('matched-headers')).toContainText(
'x-factory-two',
)
})
Loading