首页 / 文章

调试 Next.js Server Actions:部署错误、CORS 问题及负载限制

一本实用的故障排除指南,解释了为何 Next.js Server Actions 和 API 路由会静默失败或出现难以理解的错误,并为每种情况提供了具体的解决方案。

发布者:REACTAPP.TOP阅读约 6 分钟1420 词

来源: Common Server Action and API Route Errors in Next.js (and How to Fix Them)

Next.js中的Server Actions大大减少了后端逻辑的复杂性——处理表单提交不再需要专门的API路由。但这种便利性也带来了一系列故障模式,这些故障与旧版Pages Router中的错误截然不同。以下是常见问题及其解决方法。

1. 部署后出现“错误:无法找到Server Action”

在本地测试时一切正常,但新版本发布后立刻出问题——正在使用服务的用户会突然遇到此错误。

原因:Server Actions在编译时会绑定特定构建版本的ID。如果在部署时有用户打开浏览器标签页,他们的客户端会继续引用旧版本中的动作ID,而服务器已不再识别这些ID。

解决方案:虽然无法完全防止正在执行部署的用户遇到此问题,但可以更妥善地处理它,而非让其静默崩溃:

'use client';
import { useTransition } from 'react';
function SubmitButton({ action }) {
  const [isPending, startTransition] = useTransition();  async function handleClick() {
    try {
      await action();
    } catch (err) {
      if (err.message?.includes('Failed to find Server Action')) {
        window.location.reload(); // force a fresh client with current build IDs
      }
    }
  }  return <button onClick={() => startTransition(handleClick)}>Submit</button>;
}

捕获这种特定的错误并提示用户刷新,比直接出现无解释的崩溃体验要好得多。

2. 服务器操作未正常执行

调用了相应操作,却没有任何反应,且也没有任何错误信息显示。

常见原因:函数开头缺少'use server'指令,或者将操作写在客户端组件中且未正确标记。

// Wrong — no directive
async function saveUser(formData) {
  await db.user.update(/* ... */);
}

解决方案:

'use server';
export async function saveUser(formData) {
  await db.user.update(/* ... */);
}

如果该操作与客户端组件定义在同一个文件中,那么该指令需要放在函数本身的最顶部,而不仅仅是文件的最顶部:

'use client';
export default function Form() {
  async function saveUser(formData) {
    'use server'; // required here specifically
    await db.user.update(/* ... */);
  }
  return <form action={saveUser}>{/* ... */}</form>;
}

3. “错误:只能将普通对象传递给客户端组件”

当服务器操作的返回值不可序列化时会出现此错误——例如类实例、嵌藏在复杂结构中的Date对象,或是带有自身方法的数据库模型。

'use server';
export async function getUser(id) {
  return await db.user.findUnique({ where: { id } }); // ORM might return a class instance
}

解决方案——手动将结果转换为普通对象:

'use server';
export async function getUser(id) {
  const user = await db.user.findUnique({ where: { id } });
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    createdAt: user.createdAt.toISOString(), // Dates need explicit serialization too
  };
}

不要直接将ORM查询结果传递给客户端组件,应先将其转换为普通的、可序列化的对象。

4. 服务器操作正常,但表单数据返回为undefined

<form action={createPost}>
  <input name="title" />
</form>
'use server';
export async function createPost(formData) {
  console.log(formData.title); // undefined
}

原因:你接收到的formData参数是一个原始的FormData实例,而非普通的JavaScript对象,因此必须调用.get()方法来获取其中的值。

解决方案:

'use server';
export async function createPost(formData) {
  const title = formData.get('title');
  console.log(title); // correct
}

5. Server Actions中的“请求体大小超过1MB限制”问题

Server Actions对请求体的大小设有默认上限,一旦需要处理文件上传或体积较大的表单数据,就会出现问题。

解决方案——在next.config.js中明确提高该限制:

// next.config.js
module.exports = {
  experimental: {
    serverActions: {
      bodySizeLimit: '10mb',
    },
  },
};

对于任何类似真实文件上传的场景,通常最好完全跳过通过Server Action处理文件的操作。直接使用带签名的URL将文件从客户端上传到对象存储服务——如S3、R2等——这样可以避开大小限制,同时减轻服务器的负担。

6. 从不同源地址调用 API 路由时出现的 CORS 错误

当移动应用、浏览器扩展或独立前端直接访问 Next.js 的 API 路由,而非通过自有页面时,就容易出现这种问题。

Access to fetch at 'https://yourapp.com/api/data' from origin 'https://otherapp.com' has been blocked by CORS policy

解决方案——在路由处理函数中显式设置响应头:

// app/api/data/route.js
export async function GET(request) {
  const data = await getData();
  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': 'https://otherapp.com', // never '*' for authenticated routes
      'Access-Control-Allow-Methods': 'GET, POST',
    },
  });
}
export async function OPTIONS() {
  return new Response(null, {
    headers: {
      'Access-Control-Allow-Origin': 'https://otherapp.com',
      'Access-Control-Allow-Methods': 'GET, POST',
    },
  });
}

对于依赖身份验证 Cookie 的任何接口,都应避免设置 Access-Control-Allow-Origin: '*',因为这样会将该用户数据交给任何试图获取它的源地址。

7. 路由处理函数在出错时返回 HTML 而非 JSON

当路由处理函数抛出未处理的异常时,客户端不会收到格式规范的 JSON 错误信息,而是会得到 Next.js 内置的 HTML 错误页面。这对于那些假设可以安全调用 response.json() 的客户端代码来说是个问题。

解决方案——将处理逻辑封装在始终返回 JSON 的 try/catch 结构中:

export async function POST(request) {
  try {
    const body = await request.json();
    const result = await processOrder(body);
    return Response.json(result);
  } catch (error) {
    return Response.json(
      { error: error.message ?? 'Something went wrong' },
      { status: 500 }
    );
  }
}

采用这种方式后,无论请求成功还是失败,客户端都能收到可解析为 JSON 的数据,而不会收到会导致后续 .json() 调用出错的 HTML 文档。

为何这些错误比传统 API 路由的故障更糟糕

服务器端操作将客户端逻辑与服务器端逻辑合并到同一个文件中,这使得人们在不知不觉间就越过了边界。某个返回值在编辑器中可能看起来完全正常,但由于不可序列化而在运行时出现故障;又或者某个指令缺失后不会像预期那样抛出异常,而是悄无声息地导致问题。针对 REST 风格 API 的常规调试方法——打开网络标签页查看响应——在这里并不总是有效,因为许多这类故障甚至在任何网络请求被发送之前就已经发生了。

对于那些需要构建包含大量表单、文件上传功能以及分布在多页上的数据变更操作的仪表板团队而言,最好在代码库中统一解决序列化、错误处理以及请求体大小限制等问题,而非在每个新表单中反复调试同样的问题。一些专为人工智能驱动的SaaS产品设计的预构建管理模板已经内置了正确配置的Server Actions和API路由,还包括通过Zod进行的验证以及从一开始就完善的错误处理机制,这样这七类问题只需解决一次,而不会在你编写的每个表单中再次出现。

总结

服务器端操作减少了API路由过去所需的许多冗余代码,但同时也带来了新的故障模式:部署后构建ID失效、缺失指令、无法序列化的返回值、FormData的误用、请求体大小限制、跨源CORS错误,以及并非真正JSON格式的错误响应。这七种问题都很常见,每一种都只需几行代码就能解决,在认为存在更复杂的问题之前,都值得先快速检查一下。