Skip to content
Open
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
184 changes: 181 additions & 3 deletions docs/start/framework/react/guide/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,11 +520,11 @@ Middleware that uses the `server` method executes in the same context as server

### Modifying the Client Request

Middleware that uses the `client` method executes in a **completely different client-side context** than server functions, so you can't use the same utilities to read and modify the request. However, you can still modify the request returning additional properties when calling the `next` function. Currently supported properties are:
Middleware that uses the `client` method executes in a **completely different client-side context** than server functions, so you can't use the same utilities to read and modify the request. However, you can still modify the request by returning additional properties when calling the `next` function.

- `headers`: An object containing headers to be added to the request.
#### Setting Custom Headers

Here's an example of adding an `Authorization` header any request using this middleware:
You can add headers to the outgoing request by passing a `headers` object to `next`:

```tsx
import { getToken } from 'my-auth-library'
Expand All @@ -540,6 +540,184 @@ const authMiddleware = createMiddleware({ type: 'function' }).client(
)
```

#### Header Merging Across Middleware

When multiple middlewares set headers, they are **merged together**. Later middlewares can add new headers or override headers set by earlier middlewares:

```tsx
const firstMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
return next({
headers: {
'X-Request-ID': '12345',
'X-Source': 'first-middleware',
},
})
},
)

const secondMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
return next({
headers: {
'X-Timestamp': Date.now().toString(),
'X-Source': 'second-middleware', // Overrides first middleware
},
})
},
)

// Final headers will include:
// - X-Request-ID: '12345' (from first)
// - X-Timestamp: '<timestamp>' (from second)
// - X-Source: 'second-middleware' (second overrides first)
```

You can also set headers directly at the call site:

```tsx
await myServerFn({
data: { name: 'John' },
headers: {
'X-Custom-Header': 'call-site-value',
},
})
```

**Header precedence (all headers are merged, later values override earlier):**

1. Earlier middleware headers
2. Later middleware headers (override earlier)
3. Call-site headers (override all middleware headers)

#### Custom Fetch Implementation

For advanced use cases, you can provide a custom `fetch` implementation to control how server function requests are made. This is useful for:

- Adding request interceptors or retry logic
- Using a custom HTTP client
- Testing and mocking
- Adding telemetry or monitoring

**Via Client Middleware:**

```tsx
import type { CustomFetch } from '@tanstack/react-start'

const customFetchMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const customFetch: CustomFetch = async (url, init) => {
console.log('Request starting:', url)
const start = Date.now()

const response = await fetch(url, init)

console.log('Request completed in', Date.now() - start, 'ms')
return response
}

return next({ fetch: customFetch })
},
)
```

**Directly at Call Site:**

```tsx
import type { CustomFetch } from '@tanstack/react-start'

const myFetch: CustomFetch = async (url, init) => {
// Add custom logic here
return fetch(url, init)
}

await myServerFn({
data: { name: 'John' },
fetch: myFetch,
})
```

#### Fetch Override Precedence

When custom fetch implementations are provided at multiple levels, the following precedence applies (highest to lowest priority):

| Priority | Source | Description |
| ----------- | ------------------ | ----------------------------------------------- |
| 1 (highest) | Call site | `serverFn({ fetch: customFetch })` |
| 2 | Later middleware | Last middleware in chain that provides `fetch` |
| 3 | Earlier middleware | First middleware in chain that provides `fetch` |
| 4 (lowest) | Default | Global `fetch` function |

**Key principle:** The call site always wins. This allows you to override middleware behavior for specific calls when needed.

```tsx
// Middleware sets a fetch that adds logging
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const loggingFetch: CustomFetch = async (url, init) => {
console.log('Middleware fetch:', url)
return fetch(url, init)
}
return next({ fetch: loggingFetch })
},
)

const myServerFn = createServerFn()
.middleware([loggingMiddleware])
.handler(async () => {
return { message: 'Hello' }
})

// Uses middleware's loggingFetch
await myServerFn()

// Override with custom fetch for this specific call
const testFetch: CustomFetch = async (url, init) => {
console.log('Test fetch:', url)
return fetch(url, init)
}
await myServerFn({ fetch: testFetch }) // Uses testFetch, NOT loggingFetch
```

**Chained Middleware Example:**

When multiple middlewares provide fetch, the last one wins:

```tsx
const firstMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const firstFetch: CustomFetch = (url, init) => {
const headers = new Headers(init?.headers)
headers.set('X-From', 'first-middleware')
return fetch(url, { ...init, headers })
}
return next({ fetch: firstFetch })
},
)

const secondMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const secondFetch: CustomFetch = (url, init) => {
const headers = new Headers(init?.headers)
headers.set('X-From', 'second-middleware')
return fetch(url, { ...init, headers })
}
return next({ fetch: secondFetch })
},
)

const myServerFn = createServerFn()
.middleware([firstMiddleware, secondMiddleware])
.handler(async () => {
// Request will have X-From: 'second-middleware'
// because secondMiddleware's fetch overrides firstMiddleware's fetch
return { message: 'Hello' }
})
```

> [!NOTE]
> Custom fetch only applies on the client side. During SSR, server functions are called directly without going through fetch.

## Environment and Performance

### Environment Tree Shaking
Expand Down
21 changes: 21 additions & 0 deletions e2e/react-start/server-functions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Route as HeadersRouteImport } from './routes/headers'
import { Route as FormdataContextRouteImport } from './routes/formdata-context'
import { Route as EnvOnlyRouteImport } from './routes/env-only'
import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserve'
import { Route as CustomFetchRouteImport } from './routes/custom-fetch'
import { Route as ConsistentRouteImport } from './routes/consistent'
import { Route as AsyncValidationRouteImport } from './routes/async-validation'
import { Route as IndexRouteImport } from './routes/index'
Expand Down Expand Up @@ -112,6 +113,11 @@ const DeadCodePreserveRoute = DeadCodePreserveRouteImport.update({
path: '/dead-code-preserve',
getParentRoute: () => rootRouteImport,
} as any)
const CustomFetchRoute = CustomFetchRouteImport.update({
id: '/custom-fetch',
path: '/custom-fetch',
getParentRoute: () => rootRouteImport,
} as any)
const ConsistentRoute = ConsistentRouteImport.update({
id: '/consistent',
path: '/consistent',
Expand Down Expand Up @@ -245,6 +251,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/async-validation': typeof AsyncValidationRoute
'/consistent': typeof ConsistentRoute
'/custom-fetch': typeof CustomFetchRoute
'/dead-code-preserve': typeof DeadCodePreserveRoute
'/env-only': typeof EnvOnlyRoute
'/formdata-context': typeof FormdataContextRoute
Expand Down Expand Up @@ -284,6 +291,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/async-validation': typeof AsyncValidationRoute
'/consistent': typeof ConsistentRoute
'/custom-fetch': typeof CustomFetchRoute
'/dead-code-preserve': typeof DeadCodePreserveRoute
'/env-only': typeof EnvOnlyRoute
'/formdata-context': typeof FormdataContextRoute
Expand Down Expand Up @@ -324,6 +332,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/async-validation': typeof AsyncValidationRoute
'/consistent': typeof ConsistentRoute
'/custom-fetch': typeof CustomFetchRoute
'/dead-code-preserve': typeof DeadCodePreserveRoute
'/env-only': typeof EnvOnlyRoute
'/formdata-context': typeof FormdataContextRoute
Expand Down Expand Up @@ -365,6 +374,7 @@ export interface FileRouteTypes {
| '/'
| '/async-validation'
| '/consistent'
| '/custom-fetch'
| '/dead-code-preserve'
| '/env-only'
| '/formdata-context'
Expand Down Expand Up @@ -404,6 +414,7 @@ export interface FileRouteTypes {
| '/'
| '/async-validation'
| '/consistent'
| '/custom-fetch'
| '/dead-code-preserve'
| '/env-only'
| '/formdata-context'
Expand Down Expand Up @@ -443,6 +454,7 @@ export interface FileRouteTypes {
| '/'
| '/async-validation'
| '/consistent'
| '/custom-fetch'
| '/dead-code-preserve'
| '/env-only'
| '/formdata-context'
Expand Down Expand Up @@ -483,6 +495,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AsyncValidationRoute: typeof AsyncValidationRoute
ConsistentRoute: typeof ConsistentRoute
CustomFetchRoute: typeof CustomFetchRoute
DeadCodePreserveRoute: typeof DeadCodePreserveRoute
EnvOnlyRoute: typeof EnvOnlyRoute
FormdataContextRoute: typeof FormdataContextRoute
Expand Down Expand Up @@ -612,6 +625,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DeadCodePreserveRouteImport
parentRoute: typeof rootRouteImport
}
'/custom-fetch': {
id: '/custom-fetch'
path: '/custom-fetch'
fullPath: '/custom-fetch'
preLoaderRoute: typeof CustomFetchRouteImport
parentRoute: typeof rootRouteImport
}
'/consistent': {
id: '/consistent'
path: '/consistent'
Expand Down Expand Up @@ -787,6 +807,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AsyncValidationRoute: AsyncValidationRoute,
ConsistentRoute: ConsistentRoute,
CustomFetchRoute: CustomFetchRoute,
DeadCodePreserveRoute: DeadCodePreserveRoute,
EnvOnlyRoute: EnvOnlyRoute,
FormdataContextRoute: FormdataContextRoute,
Expand Down
Loading
Loading