mcpField
M
middleware

middleware

github.com/tanstack开发15.3k195 次安装4 天前更新
代码仓库

关于 middleware

面向 TanStack Start 的技能,介绍如何组合请求与服务端函数中间件,并处理验证、上下文和授权。

主要功能

  1. 01请求与函数中间件
  2. 02可组合的中间件链
  3. 03客户端与服务端阶段
  4. 04验证与上下文传递
  5. 05路由及全局中间件

适用场景

  1. 01为服务端函数添加会话检查
  2. 02在中间件间传递上下文
  3. 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())

交叉引用

评论

支持 Markdown