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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,36 @@ import { createFileRoute } from '@tanstack/react-router'
import { createMiddleware, createServerFn } from '@tanstack/react-start'
import React from 'react'

// Request middleware that captures serverFnMeta
// Note: Request middleware only runs server-side, so it receives full ServerFnMeta
// serverFnMeta is only present for server function calls, undefined for regular page requests
const requestMetadataMiddleware = createMiddleware({ type: 'request' }).server(
async ({ next, serverFnMeta }) => {
return next({
context: {
requestCapturedMeta: serverFnMeta,
},
})
},
)

// Separate request middleware for route-level server middleware
// This will receive serverFnMeta as undefined for page requests
const pageRequestMiddleware = createMiddleware({ type: 'request' }).server(
async ({ next, serverFnMeta }) => {
return next({
context: {
// For page requests (not server function calls), serverFnMeta should be undefined
// We use '$undefined' string to prove we actually executed and passed data through
pageRequestServerFnMeta:
serverFnMeta === undefined ? '$undefined' : serverFnMeta,
},
})
},
)

const metadataMiddleware = createMiddleware({ type: 'function' })
.middleware([requestMetadataMiddleware])
.client(async ({ next, serverFnMeta }) => {
return next({
sendContext: {
Expand All @@ -15,11 +44,12 @@ const metadataMiddleware = createMiddleware({ type: 'function' })
context: {
serverCapturedMeta: serverFnMeta,
clientCapturedMeta: context.clientCapturedMeta,
requestCapturedMeta: context.requestCapturedMeta,
},
})
})

// Server function that returns both client and server captured metadata
// Server function that returns client, server, and request captured metadata
const getMetadataFn = createServerFn()
.middleware([metadataMiddleware])
.handler(async ({ context }) => {
Expand All @@ -29,16 +59,43 @@ const getMetadataFn = createServerFn()
// Metadata captured by client middleware and sent via sendContext
// Client middleware only has { id }, not { name, filename }
clientCapturedMeta: context.clientCapturedMeta,
// Metadata captured by request middleware
// Request middleware receives full ServerFnMeta for server function calls
// or undefined for regular page requests
requestCapturedMeta: context.requestCapturedMeta,
}
})

export const Route = createFileRoute('/middleware/function-metadata')({
// Server route configuration to test that serverFnMeta is undefined for page requests
server: {
middleware: [pageRequestMiddleware],
handlers: {
GET: async ({ next, context }) => {
// Pass the captured serverFnMeta (should be undefined for page requests) to serverContext
return next({
context: {
pageRequestServerFnMeta: context.pageRequestServerFnMeta,
},
})
},
},
},
// Access serverContext in beforeLoad to pass to route context
beforeLoad: async ({ serverContext }) => {
return {
// serverContext contains data from GET handler + middleware context
// For page requests, pageRequestServerFnMeta should be undefined
pageRequestServerFnMeta: serverContext?.pageRequestServerFnMeta,
}
},
loader: () => getMetadataFn(),
component: RouteComponent,
})

function RouteComponent() {
const loaderData = Route.useLoaderData()
const routeContext = Route.useRouteContext()

const [clientData, setClientData] = React.useState<typeof loaderData | null>(
null,
Expand All @@ -48,12 +105,30 @@ function RouteComponent() {
<div>
<h2>Function Metadata in Middleware</h2>
<p>
This test verifies that both client and server middleware receive
This test verifies that client, server, and request middleware receive
serverFnMeta in their options. Client middleware gets only the id, while
server middleware gets the full metadata (id, name, filename).
server and request middleware get the full metadata (id, name,
filename). Request middleware only receives serverFnMeta for server
function calls, not for regular page requests.
</p>
<br />
<div>
<div data-testid="page-request-data">
<h3>Page Request Middleware Data (via serverContext)</h3>
<p>
For regular page requests (not server function calls), serverFnMeta
should be undefined:
</p>
<div>
Page Request serverFnMeta:{' '}
<span data-testid="page-request-server-fn-meta">
{typeof routeContext.pageRequestServerFnMeta === 'string'
? routeContext.pageRequestServerFnMeta
: JSON.stringify(routeContext.pageRequestServerFnMeta)}
</span>
</div>
</div>
<br />
<div data-testid="loader-data">
<h3>Loader Data (SSR)</h3>
<h4>Server Captured Metadata:</h4>
Expand Down Expand Up @@ -97,6 +172,29 @@ function RouteComponent() {
{(loaderData.clientCapturedMeta as any)?.filename ?? 'undefined'}
</span>
</div>
<h4>Request Middleware Captured Metadata:</h4>
<p>
Request middleware receives full ServerFnMeta for server function
calls:
</p>
<div>
Request Captured ID:{' '}
<span data-testid="loader-request-captured-id">
{loaderData.requestCapturedMeta?.id ?? 'undefined'}
</span>
</div>
<div>
Request Captured Name:{' '}
<span data-testid="loader-request-captured-name">
{loaderData.requestCapturedMeta?.name ?? 'undefined'}
</span>
</div>
<div>
Request Captured Filename:{' '}
<span data-testid="loader-request-captured-filename">
{loaderData.requestCapturedMeta?.filename ?? 'undefined'}
</span>
</div>
</div>
<br />
<div>
Expand Down Expand Up @@ -156,6 +254,29 @@ function RouteComponent() {
'undefined'}
</span>
</div>
<h4>Request Middleware Captured Metadata:</h4>
<p>
Request middleware receives full ServerFnMeta for server function
calls:
</p>
<div>
Request Captured ID:{' '}
<span data-testid="client-request-captured-id">
{clientData.requestCapturedMeta?.id ?? 'undefined'}
</span>
</div>
<div>
Request Captured Name:{' '}
<span data-testid="client-request-captured-name">
{clientData.requestCapturedMeta?.name ?? 'undefined'}
</span>
</div>
<div>
Request Captured Filename:{' '}
<span data-testid="client-request-captured-filename">
{clientData.requestCapturedMeta?.filename ?? 'undefined'}
</span>
</div>
</div>
)}
</div>
Expand Down
45 changes: 44 additions & 1 deletion e2e/react-start/server-functions/tests/server-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,11 +764,21 @@ test('function middleware receives serverFnMeta in options', async ({
// This test verifies that:
// 1. Client middleware receives serverFnMeta with just { id } - NOT name or filename
// 2. Server middleware receives serverFnMeta with full { id, name, filename }
// 3. Client middleware can send the function metadata to the server via sendContext
// 3. Request middleware receives serverFnMeta with full { id, name, filename } for server function calls
// 4. Request middleware receives serverFnMeta as undefined for page requests (not server function calls)
// 5. Client middleware can send the function metadata to the server via sendContext
await page.goto('/middleware/function-metadata')

await page.waitForLoadState('networkidle')

// First, verify that for page requests, serverFnMeta is undefined
// This is captured by the route-level server middleware and passed via serverContext
// The middleware sets '$undefined' string to prove we actually executed and passed data through
const pageRequestServerFnMeta = await page
.getByTestId('page-request-server-fn-meta')
.textContent()
expect(pageRequestServerFnMeta).toBe('$undefined')

// Verify SSR data - server captured metadata should have full properties
const loaderFunctionId = await page
.getByTestId('loader-function-id')
Expand All @@ -786,6 +796,15 @@ test('function middleware receives serverFnMeta in options', async ({
const loaderClientCapturedFilename = await page
.getByTestId('loader-client-captured-filename')
.textContent()
const loaderRequestCapturedId = await page
.getByTestId('loader-request-captured-id')
.textContent()
const loaderRequestCapturedName = await page
.getByTestId('loader-request-captured-name')
.textContent()
const loaderRequestCapturedFilename = await page
.getByTestId('loader-request-captured-filename')
.textContent()

// id should be a non-empty string
expect(loaderFunctionId).toBeTruthy()
Expand All @@ -807,6 +826,14 @@ test('function middleware receives serverFnMeta in options', async ({
expect(loaderClientCapturedName).toBe('undefined')
expect(loaderClientCapturedFilename).toBe('undefined')

// Request middleware should have full metadata (id, name, filename)
// since it runs server-side during server function calls
expect(loaderRequestCapturedId).toBe(loaderFunctionId)
expect(loaderRequestCapturedName).toBe('getMetadataFn')
expect(loaderRequestCapturedFilename).toBe(
'src/routes/middleware/function-metadata.tsx',
)

// Now test client-side call
await page.getByTestId('call-server-fn-btn').click()
await page.waitForSelector('[data-testid="client-data"]')
Expand All @@ -827,6 +854,15 @@ test('function middleware receives serverFnMeta in options', async ({
const clientClientCapturedFilename = await page
.getByTestId('client-client-captured-filename')
.textContent()
const clientRequestCapturedId = await page
.getByTestId('client-request-captured-id')
.textContent()
const clientRequestCapturedName = await page
.getByTestId('client-request-captured-name')
.textContent()
const clientRequestCapturedFilename = await page
.getByTestId('client-request-captured-filename')
.textContent()

// Client call should get the same server metadata
expect(clientFunctionId).toBe(loaderFunctionId)
Expand All @@ -839,4 +875,11 @@ test('function middleware receives serverFnMeta in options', async ({
// Client middleware should NOT have access to name or filename
expect(clientClientCapturedName).toBe('undefined')
expect(clientClientCapturedFilename).toBe('undefined')

// Request middleware should have full metadata for client-side calls too
expect(clientRequestCapturedId).toBe(loaderFunctionId)
expect(clientRequestCapturedName).toBe('getMetadataFn')
expect(clientRequestCapturedFilename).toBe(
'src/routes/middleware/function-metadata.tsx',
)
})
6 changes: 6 additions & 0 deletions packages/start-client-core/src/createMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,12 @@ export interface RequestServerOptions<TRegister, TMiddlewares> {
pathname: string
context: Expand<AssignAllServerRequestContext<TRegister, TMiddlewares>>
next: RequestServerNextFn<TRegister, TMiddlewares>
/**
* Metadata about the server function being invoked.
* This is only present when the request is handling a server function call.
* For regular page requests, this will be undefined.
*/
serverFnMeta?: ServerFnMeta
}

export type RequestServerNextFn<TRegister, TMiddlewares> = <
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createMiddleware } from '../createMiddleware'
import type { RequestServerNextFn } from '../createMiddleware'
import type { ConstrainValidator } from '../createServerFn'
import type { Register } from '@tanstack/router-core'
import type { ServerFnMeta } from '../constants'

test('createServeMiddleware removes middleware after middleware,', () => {
const middleware = createMiddleware({ type: 'function' })
Expand Down Expand Up @@ -659,6 +660,7 @@ test('createMiddleware with type request, no middleware or context', () => {
next: RequestServerNextFn<{}, undefined>
pathname: string
context: undefined
serverFnMeta?: ServerFnMeta
}>()

const result = await options.next()
Expand All @@ -681,6 +683,7 @@ test('createMiddleware with type request, no middleware with context', () => {
next: RequestServerNextFn<{}, undefined>
pathname: string
context: undefined
serverFnMeta?: ServerFnMeta
}>()

const result = await options.next({ context: { a: 'a' } })
Expand All @@ -704,6 +707,7 @@ test('createMiddleware with type request, middleware and context', () => {
next: RequestServerNextFn<{}, undefined>
pathname: string
context: undefined
serverFnMeta?: ServerFnMeta
}>()

const result = await options.next({ context: { a: 'a' } })
Expand All @@ -727,6 +731,7 @@ test('createMiddleware with type request, middleware and context', () => {
next: RequestServerNextFn<{}, undefined>
pathname: string
context: { a: string }
serverFnMeta?: ServerFnMeta
}>()

const result = await options.next({ context: { b: 'b' } })
Expand All @@ -749,6 +754,7 @@ test('createMiddleware with type request can return Response directly', () => {
next: RequestServerNextFn<{}, undefined>
pathname: string
context: undefined
serverFnMeta?: ServerFnMeta
}>()

// Should be able to return a Response directly
Expand All @@ -768,6 +774,7 @@ test('createMiddleware with type request can return Promise<Response>', () => {
next: RequestServerNextFn<{}, undefined>
pathname: string
context: undefined
serverFnMeta?: ServerFnMeta
}>()

// Should be able to return a Promise<Response>
Expand All @@ -782,6 +789,7 @@ test('createMiddleware with type request can return sync Response', () => {
next: RequestServerNextFn<{}, undefined>
pathname: string
context: undefined
serverFnMeta?: ServerFnMeta
}>()

// Should be able to return a synchronous Response
Expand Down
Loading