diff --git a/e2e/react-start/server-functions/src/routes/middleware/function-metadata.tsx b/e2e/react-start/server-functions/src/routes/middleware/function-metadata.tsx index e543862e91..0ae18d4756 100644 --- a/e2e/react-start/server-functions/src/routes/middleware/function-metadata.tsx +++ b/e2e/react-start/server-functions/src/routes/middleware/function-metadata.tsx @@ -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: { @@ -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 }) => { @@ -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( null, @@ -48,12 +105,30 @@ function RouteComponent() {

Function Metadata in Middleware

- 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.


+
+

Page Request Middleware Data (via serverContext)

+

+ For regular page requests (not server function calls), serverFnMeta + should be undefined: +

+
+ Page Request serverFnMeta:{' '} + + {typeof routeContext.pageRequestServerFnMeta === 'string' + ? routeContext.pageRequestServerFnMeta + : JSON.stringify(routeContext.pageRequestServerFnMeta)} + +
+
+

Loader Data (SSR)

Server Captured Metadata:

@@ -97,6 +172,29 @@ function RouteComponent() { {(loaderData.clientCapturedMeta as any)?.filename ?? 'undefined'}
+

Request Middleware Captured Metadata:

+

+ Request middleware receives full ServerFnMeta for server function + calls: +

+
+ Request Captured ID:{' '} + + {loaderData.requestCapturedMeta?.id ?? 'undefined'} + +
+
+ Request Captured Name:{' '} + + {loaderData.requestCapturedMeta?.name ?? 'undefined'} + +
+
+ Request Captured Filename:{' '} + + {loaderData.requestCapturedMeta?.filename ?? 'undefined'} + +

@@ -156,6 +254,29 @@ function RouteComponent() { 'undefined'}
+

Request Middleware Captured Metadata:

+

+ Request middleware receives full ServerFnMeta for server function + calls: +

+
+ Request Captured ID:{' '} + + {clientData.requestCapturedMeta?.id ?? 'undefined'} + +
+
+ Request Captured Name:{' '} + + {clientData.requestCapturedMeta?.name ?? 'undefined'} + +
+
+ Request Captured Filename:{' '} + + {clientData.requestCapturedMeta?.filename ?? 'undefined'} + +
)} diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts index 75419754ba..0f189b5cec 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -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') @@ -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() @@ -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"]') @@ -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) @@ -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', + ) }) diff --git a/packages/start-client-core/src/createMiddleware.ts b/packages/start-client-core/src/createMiddleware.ts index 2505941104..8374059daa 100644 --- a/packages/start-client-core/src/createMiddleware.ts +++ b/packages/start-client-core/src/createMiddleware.ts @@ -753,6 +753,12 @@ export interface RequestServerOptions { pathname: string context: Expand> next: RequestServerNextFn + /** + * 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 = < diff --git a/packages/start-client-core/src/tests/createServerMiddleware.test-d.ts b/packages/start-client-core/src/tests/createServerMiddleware.test-d.ts index 11bfb6598c..323797d3f3 100644 --- a/packages/start-client-core/src/tests/createServerMiddleware.test-d.ts +++ b/packages/start-client-core/src/tests/createServerMiddleware.test-d.ts @@ -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' }) @@ -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() @@ -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' } }) @@ -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' } }) @@ -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' } }) @@ -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 @@ -768,6 +774,7 @@ test('createMiddleware with type request can return Promise', () => { next: RequestServerNextFn<{}, undefined> pathname: string context: undefined + serverFnMeta?: ServerFnMeta }>() // Should be able to return a Promise @@ -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