关于 middleware
面向 TanStack Start 的技能,介绍如何组合请求与服务端函数中间件,并处理验证、上下文和授权。
主要功能
- 01请求与函数中间件
- 02可组合的中间件链
- 03客户端与服务端阶段
- 04验证与上下文传递
- 05路由及全局中间件
适用场景
- 01为服务端函数添加会话检查
- 02在中间件间传递上下文
- 03为路由处理器应用中间件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill middleware安装后在 Claude Code 中输入/middleware确认已加载。
详细介绍
中间件
中间件用于自定义服务端函数和服务端路由的行为。它具有可组合性——中间件可以依赖其他中间件,组成一个调用链。
CRITICAL:TypeScript 会强制要求方法按以下顺序调用:
middleware()→validator()→client()→server()。顺序错误会导致类型错误。 CRITICAL:验证sendContext的_形状_(例如,z.string().uuid().parse(...))不等于授权。解析后的标识符只是格式正确的标识符,并不代表已获得授权。使用客户端发送的 ID 作为查询键、筛选条件或路径参数前,务必根据会话主体重新检查访问权限。
两种中间件
| 特性 | 请求中间件 | 服务端函数中间件 |
|---|---|---|
| 作用范围 | 所有服务端请求(SSR、路由、函数) | 仅服务端函数 |
| 方法 | .server() | .client()、.server() |
| 输入验证 | 否 | 是(.validator()) |
| 客户端逻辑 | 否 | 是 |
| 创建方式 | createMiddleware() | createMiddleware({ type: 'function' }) |
请求中间件不能依赖服务端函数中间件。服务端函数中间件可以依赖这两种中间件。
请求中间件
在所有服务端请求(SSR、服务端路由、服务端函数)中运行:
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware().server(
async ({ next, context, request }) => {
console.log('Request:', request.url)
const result = await next()
return result
},
)
服务端函数中间件
包含客户端和服务端两个阶段:
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createMiddleware } from '@tanstack/react-start'
const authMiddleware = createMiddleware({ type: 'function' })
.client(async ({ next }) => {
// Runs on client BEFORE the RPC call
const result = await next()
// Runs on client AFTER the RPC response
return result
})
.server(async ({ next, context }) => {
// Runs on server BEFORE the handler
const result = await next()
// Runs on server AFTER the handler
return result
})
将中间件附加到服务端函数
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerFn } from '@tanstack/react-start'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
// context contains data from middleware
return { user: context.user }
})
通过 next() 传递上下文
沿中间件调用链向下传递上下文:
const authMiddleware = createMiddleware().server(async ({ next, request }) => {
const session = await getSession(request.headers)
if (!session) throw new Error('Unauthorized')
return next({
context: { session },
})
})
const roleMiddleware = createMiddleware()
.middleware([authMiddleware])
.server(async ({ next, context }) => {
console.log('Session:', context.session) // typed!
return next()
})
在客户端和服务端之间传递上下文
客户端 → 服务端(sendContext)
const workspaceMiddleware = createMiddleware({ type: 'function' })
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, context }) => {
// workspaceId available here, but VALIDATE IT
console.log('Workspace:', context.workspaceId)
return next()
})
服务端 → 客户端(服务端中的 sendContext)
const serverTimer = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
return next({
sendContext: {
timeFromServer: new Date(),
},
})
},
)
const clientLogger = createMiddleware({ type: 'function' })
.middleware([serverTimer])
.client(async ({ next }) => {
const result = await next()
console.log('Server time:', result.context.timeFromServer)
return result
})
在中间件中验证输入
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'
const workspaceMiddleware = createMiddleware({ type: 'function' })
.validator(zodValidator(z.object({ workspaceId: z.string() })))
.server(async ({ next, data }) => {
console.log('Workspace:', data.workspaceId)
return next()
})
全局中间件
创建 src/start.ts 以配置全局中间件:
// src/start.ts
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createStart, createMiddleware } from '@tanstack/react-start'
const requestLogger = createMiddleware().server(async ({ next, request }) => {
console.log(`${request.method} ${request.url}`)
return next()
})
const functionAuth = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
// runs for every server function
return next()
},
)
export const startInstance = createStart(() => ({
requestMiddleware: [requestLogger],
functionMiddleware: [functionAuth],
}))
在服务端路由中使用中间件
路由中的所有处理程序
export const Route = createFileRoute('/api/users')({
server: {
middleware: [authMiddleware],
handlers: {
GET: async ({ context }) => Response.json(context.user),
POST: async ({ request }) => {
/* ... */
},
},
},
})
仅特定处理程序
export const Route = createFileRoute('/api/users')({
server: {
handlers: ({ createHandlers }) =>
createHandlers({
GET: async () => Response.json({ public: true }),
POST: {
middleware: [authMiddleware],
handler: async ({ context }) => {
return Response.json({ user: context.session.user })
},
},
}),
},
})
中间件工厂
创建带参数的中间件,用于授权等可复用模式:
const authMiddleware = createMiddleware().server(async ({ next, request }) => {
const session = await auth.getSession({ headers: request.headers })
if (!session) throw new Error('Unauthorized')
return next({ context: { session } })
})
将
authMiddleware附加到每个需要身份验证的createServerFn上。 服务端函数是 API 端点;路由的beforeLoad不会保护其中的数据,只会保护路由的 UI。应保护读取或修改私有数据的端点。请参阅@tanstack/router-core#router-core/auth-and-guards和 start-core/auth-server-primitives。
type Permissions = Record<string, string[]>
function authorizationMiddleware(permissions: Permissions) {
return createMiddleware({ type: 'function' })
.middleware([authMiddleware])
.server(async ({ next, context }) => {
const granted = await auth.hasPermission(context.session, permissions)
if (!granted) throw new Error('Forbidden')
return next()
})
}
// Usage
const getClients = createServerFn()
.middleware([authorizationMiddleware({ client: ['read'] })])
.handler(async () => {
return { message: 'The user can read clients.' }
})
自定义标头和 Fetch
从客户端中间件设置标头
const authMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
return next({
headers: { Authorization: `Bearer ${getToken()}` },
})
},
)
标头会在各个中间件之间合并。后续中间件会覆盖先前中间件的标头。调用位置设置的标头会覆盖所有中间件标头。
自定义 fetch
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import type { CustomFetch } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const customFetch: CustomFetch = async (url, init) => {
console.log('Request:', url)
return fetch(url, init)
}
return next({ fetch: customFetch })
},
)
Fetch 优先级(从高到低):调用位置 → 后续中间件 → 先前中间件 → createStart 全局配置 → 默认 fetch。
常见错误
1. CRITICAL:信任客户端的 sendContext——检查形状不等于检查访问权限
来自客户端中间件的 sendContext 到达服务端时,是不可信的客户端输入。大多数 Agent 用 Zod 解析形状后就会停止,并认为值是安全的。事实并非如此:解析后的 UUID 只是_某个_工作区,并不一定是发起请求的用户所属的工作区。如果不根据会话主体检查成员身份,你就构建了一个可以遍历租户的端点。
第 1 层——错误(未验证):
.server(async ({ next, context }) => {
// SQL-injectable AND tenant-walkable
await db.query(`SELECT * FROM workspace_${context.workspaceId}`)
return next()
})
第 2 层——仍然错误(仅检查形状):
.server(async ({ next, context }) => {
// Looks safe, isn't. UUID is well-formed but the user may not be a member.
const workspaceId = z.string().uuid().parse(context.workspaceId)
await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId])
return next()
})
第 3 层——正确(同时检查形状和访问权限):
.middleware([authMiddleware]) // session loaded from cookie, NOT from sendContext
.server(async ({ next, context }) => {
const workspaceId = z.string().uuid().parse(context.workspaceId)
// Verify the session principal can access this workspace.
const member = await db.memberships.find({
userId: context.session.userId,
workspaceId,
})
if (!member) throw new Error('Not a member of this workspace')
await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId])
return next({ context: { workspaceId } })
})
会话本身必须来自服务端可信来源(authMiddleware 中的 Cookie + DB 查询),绝不能来自 sendContext——客户端能够发送的内容,客户端就能伪造。请参阅 start-core/auth-server-primitives。
2. 中等:请求中间件与服务端函数中间件容易混淆
请求中间件会在所有请求中运行(SSR、路由、函数)。服务端函数中间件仅在调用 createServerFn 时运行,并有 .client() 方法。
3. 高:在 .client() 中使用浏览器 API 会导致 SSR 崩溃
在 SSR 期间,.client() 回调会在服务器上运行。localStorage 或 window 等仅限浏览器的 API 会抛出 ReferenceError:
// WRONG — localStorage doesn't exist on the server during SSR
const middleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const token = localStorage.getItem('token')
return next({ sendContext: { token } })
},
)
// CORRECT — use cookies/headers or guard with typeof window check
const middleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const token =
typeof window !== 'undefined' ? localStorage.getItem('token') : null
return next({ sendContext: { token } })
},
)
4. 中等:方法顺序错误
// WRONG — type error
createMiddleware({ type: 'function' })
.server(() => { ... })
.client(() => { ... })
// CORRECT — middleware → validator → client → server
createMiddleware({ type: 'function' })
.middleware([dep])
.validator(schema)
.client(({ next }) => next())
.server(({ next }) => next())
交叉引用
- start-core/server-functions — 中间件会包装什么
- start-core/server-routes — API 端点上的中间件
- start-core/auth-server-primitives — 如何构建
authMiddleware工厂本身:读取会话 Cookie、OAuth 状态、CSRF @tanstack/router-core#router-core/auth-and-guards— 路由侧的 UX 守卫;数据鉴权应放在服务端函数、服务端路由或 API 端点处理程序/中间件中