mcpField
S
safe-action-middleware

safe-action-middleware

github.com/next-safe-action开发11.2k 次安装8 周前更新
代码仓库

关于 safe-action-middleware

用于构建 next-safe-action 中间件链的参考技能,涵盖验证前后中间件、上下文传递、执行顺序及常见链式调用问题。

主要功能

  1. 01使用 .use() 链接中间件
  2. 02使用 .useValidated() 运行类型化中间件
  3. 03通过 next() 累积上下文
  4. 04指定中间件执行顺序
  5. 05处理 Schema 转换与错误

适用场景

  1. 01在 Action 执行前添加身份验证
  2. 02根据验证后的输入进行授权
  3. 03记录或审计解析后的输入
  4. 04在中间件间共享上下文

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/next-safe-action/skills --skill safe-action-middleware

安装后在 Claude Code 中输入/safe-action-middleware确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

auth-patterns.md
logging-monitoring.md
standalone-middleware.md

详细介绍

next-safe-action 中间件

快速入门

import { createSafeActionClient } from "next-safe-action";

const actionClient = createSafeActionClient();

// Add middleware with .use()
const authClient = actionClient.use(async ({ next }) => {
  const session = await getSession();
  if (!session?.user) {
    throw new Error("Unauthorized");
  }
  // Pass context to the next middleware/action via next({ ctx })
  return next({
    ctx: { userId: session.user.id },
  });
});

中间件的工作方式

  • .use() 会向调用链添加中间件——可以多次调用
  • 每次调用 .use() 都会返回一个新的客户端实例(不可变调用链)
  • 中间件按从上到下的顺序执行(按添加顺序)
  • 结果按从下到上的顺序流动(最深层的中间件/操作最先完成解析)
  • 上下文通过 next({ ctx }) 累积——每一层的 ctx 都会与上一层的 ctx 深度合并
const client = createSafeActionClient()
  .use(async ({ next }) => {
    console.log("1: before");             // Runs 1st
    const result = await next({ ctx: { a: 1 } });
    console.log("1: after");              // Runs 4th
    return result;
  })
  .use(async ({ next, ctx }) => {
    console.log("2: before", ctx.a);      // Runs 2nd, ctx.a = 1
    const result = await next({ ctx: { b: 2 } });
    console.log("2: after");              // Runs 3rd
    return result;
  });

// In .action(): ctx = { a: 1, b: 2 }

use() 中间件函数签名

async ({
  clientInput,           // Raw input from the client (unknown)
  bindArgsClientInputs,  // Raw bind args array
  ctx,                   // Accumulated context from previous middleware
  metadata,              // Metadata set via .metadata()
  next,                  // Call to proceed to next middleware/action
}) => {
  // Optionally extend context
  return next({ ctx: { /* new context properties */ } });
}

useValidated() — 验证后中间件

.useValidated() 会注册在输入验证之后运行的中间件,使其可以访问类型化的 parsedInput。默认使用 use()——只有当中间件逻辑依赖经过验证的输入时,才使用 useValidated()。

const action = authClient
  .inputSchema(z.object({ postId: z.string().uuid(), title: z.string() }))
  .useValidated(async ({ parsedInput, ctx, next }) => {
    // parsedInput is typed: { postId: string; title: string }
    const post = await db.post.findById(parsedInput.postId);
    if (!post || post.authorId !== ctx.userId) {
      throw new Error("Not authorized");
    }
    return next({ ctx: { post } });
  })
  .action(async ({ parsedInput, ctx }) => {
    // ctx.post is available and typed
    await db.post.update(ctx.post.id, { title: parsedInput.title });
  });

执行顺序

1. use() middleware            — pre-validation, runs in order added
2. Input validation            — schema parsing
3. useValidated() middleware   — post-validation, runs in order added
4. Server code (.action())     — receives final ctx and parsedInput

两种中间件调用链都遵循洋葱模型:next() 之前的代码按从上到下的顺序运行,next() 之后的代码按从下到上的顺序逐层返回。

use() 与 useValidated()

需求方法
身份验证、日志记录、速率限制(不需要输入).use()
在验证之前访问原始 clientInput.use()
根据经过验证的输入进行授权(例如,检查用户是否拥有资源).useValidated()
记录或审计经过验证/转换的输入.useValidated()
使用从解析后的输入派生的数据丰富上下文.useValidated()

useValidated() 中间件函数签名

async ({
  parsedInput,             // Validated, typed input (from inputSchema)
  clientInput,             // Raw input from the client
  bindArgsParsedInputs,    // Validated bind args tuple
  bindArgsClientInputs,    // Raw bind args array
  ctx,                     // Accumulated context from all previous middleware
  metadata,                // Metadata set via .metadata()
  next,                    // Call to proceed to next middleware/action
}) => {
  return next({ ctx: { /* new context properties */ } });
}
链式调用规则
  • 必须在 .useValidated() 之前调用 .inputSchema() 或 .bindArgsSchemas()
  • 不能在 .useValidated() 之后调用 .inputSchema() 或 .bindArgsSchemas()
  • 不能在 .useValidated() 之后调用 .use()
  • 可以链式调用多个 .useValidated()
模式转换

useValidated() 在 parsedInput 中看到的是转换后的值,而 clientInput 保留原始值:

authClient
  .inputSchema(z.string().transform((s) => s.toUpperCase()))
  .useValidated(async ({ clientInput, parsedInput, next }) => {
    console.log(clientInput);  // "hello" (original)
    console.log(parsedInput);  // "HELLO" (transformed)
    return next();
  })
错误回调中的上下文
  • use() 中间件设置的上下文在 onError/onSettled 回调中始终可用。
  • useValidated() 中间件设置的上下文是可选的(可能为 undefined)——如果验证失败,useValidated() 就不会运行,因此不会包含它添加的上下文。

相关文档

反模式

// BAD: Forgetting to return next() — action will hang
.use(async ({ next }) => {
  await doSomething();
  next({ ctx: {} }); // Missing return!
})

// GOOD: Always return the result of next()
.use(async ({ next }) => {
  await doSomething();
  return next({ ctx: {} });
})
// BAD: Catching all errors (swallows framework errors like redirect/notFound)
.use(async ({ next }) => {
  try {
    return await next({ ctx: {} });
  } catch (error) {
    return { serverError: "Something went wrong" }; // Swallows redirect!
  }
})

// GOOD: Re-throw framework errors
.use(async ({ next }) => {
  try {
    return await next({ ctx: {} });
  } catch (error) {
    if (error instanceof Error && "digest" in error) {
      throw error; // Let Next.js handle redirects, notFound, etc.
    }
    // Handle other errors
    console.error(error);
    return { serverError: "Something went wrong" };
  }
})
// BAD: useValidated() without an input schema — won't compile
const client = actionClient.useValidated(async ({ parsedInput, next }) => {
  return next();
});

// GOOD: Always define inputSchema or bindArgsSchemas before useValidated()
const client = actionClient
  .inputSchema(z.object({ id: z.string() }))
  .useValidated(async ({ parsedInput, next }) => {
    console.log(parsedInput.id); // Typed!
    return next();
  });

评论

支持 Markdown