You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js单个API路由生产环境500错误排查求助

Next.js Vercel部署后/api/articles GET路由生产环境500错误(ERR_HTTP_HEADERS_SENT)

问题背景

仅/api/articles的GET路由在Vercel生产环境返回500错误,开发环境运行正常,其余API路由均正常。已配置vercel.json处理CORS,Vercel日志显示错误为ERR_HTTP_HEADERS_SENT,但初步检查代码未发现明显重复响应。

受影响路由
https://mindescape-cms.vercel.app/api/articles | GET

vercel.json

{
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Access-Control-Allow-Credentials", "value": "true" },
        { "key": "Access-Control-Allow-Origin", "value": "*" },
        { "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" },
        {
          "key": "Access-Control-Allow-Headers",
          "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
        }
      ]
    }
  ]
}

Vercel日志信息

info  - Loaded env from /var/task/.env.local
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at new NodeError (node:internal/errors:399:5)
    at ServerResponse.setHeader (node:_http_outgoing:663:11)
    at _res.setHeader (/var/task/node_modules/next/dist/server/base-server.js:180:24)
    at sendJson (/var/task/node_modules/next/dist/server/api-utils/node.js:195:9)
    at apiRes.json (/var/task/node_modules/next/dist/server/api-utils/node.js:360:31)
    at handler (/var/task/.next/server/pages/api/articles.js:126:29)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.apiResolver (/var/task/node_modules/next/dist/server/api-utils/node.js:372:9)
    at async NextNodeServer.runApi (/var/task/node_modules/next/dist/server/next-server.js:514:9)
    at async Object.fn (/var/task/node_modules/next/dist/server/next-server.js:828:35) {
  code: 'ERR_HTTP_HEADERS_SENT'
}
RequestId: b1ea1aba-1e68-4782-bdfa-4b18c6c60cec Error: Runtime exited with error: exit status 1
Runtime.ExitError

/api/articles/index.js核心代码

const handler = async (req, res) => {
  //Check rate limit
  const rateLimitOk = rateLimiterMiddleware(req, res, rateLimiter);

  if (!rateLimitOk) return;

  if (req.method !== 'GET' && req.method !== 'POST' && req.method !== 'PATCH') {
    res.status(400).json({ message: 'Invalid request method. Accepted: GET, POST, PATCH.' });
    return;
  }

  if (req.method === 'GET') {
    let client;
    try {
      client = await connectDb();
    } catch (error) {
      res.status(500).json({ message: 'Connecting to the database failed.' });
      return;
    }

    const database = client.db('mindescape');
    const collection = database.collection('articles');

    let articles;
    try {
      articles = await collection.find({}).sort({ creationDate: -1 }).toArray();
    } catch (error) {
      res.status(500).json({ message: 'Connecting to the database failed.' });
      client.close();
      return;
    }

    const collection2 = database.collection('users');
    try {
      const users = await collection2.find({}).toArray();
      const actualDataToReturn = getArticlesWithCurrentUserData(articles, users);

      const isAuthenticated = await checkIfAuthenticated(req, res);

      if (!isAuthenticated) {
        const articlesWithoutSensitiveData = actualDataToReturn.map(article => {
          const comments =
            article.comments &&
            article.comments.map(item => {
              delete item.email;
              return { ...item };
            });
          return { ...article, comments };
        });
        res.status(200).json({
          quantity: articlesWithoutSensitiveData.length,
          articles: articlesWithoutSensitiveData,
          message: 'The list of published articles.',
        });
        client.close();
        return;
      }
      res.status(200).json({
        quantity: actualDataToReturn.length,
        articles: actualDataToReturn,
        message: 'The list of published articles.',
      });
      client.close();
      return;
    } catch (error) {
      res.status(500).json({ message: 'Retreiving published articles failed.' });
      client.close();
      return;
    }
  }

  // POST、PATCH逻辑省略...
};

问题原因分析

ERR_HTTP_HEADERS_SENT本质是代码尝试在已向客户端发送响应后,再次设置响应头或发送响应。结合代码和生产环境特性,核心问题如下:

1. checkIfAuthenticated函数可能提前发送响应

如果checkIfAuthenticated内部在验证失败时直接调用res.status().json()返回(比如未授权时返回401),但主代码未检测响应是否已发送,仍继续执行后续的if (!isAuthenticated)分支,导致重复发送响应。

2. 生产环境与开发环境的行为差异

开发环境Node.js对重复响应的容错性更高,或checkIfAuthenticated的验证逻辑在本地和生产环境存在差异(如环境变量、JWT密钥不一致),导致本地未触发重复响应,生产环境暴露问题。


修复方案

1. 增加响应发送状态检查

在调用checkIfAuthenticated后,立即检查res.headersSent判断是否已发送响应,若已发送则终止后续逻辑:

const isAuthenticated = await checkIfAuthenticated(req, res);
// 新增:如果checkIfAuthenticated已发送响应,直接终止
if (res.headersSent) {
  client.close();
  return;
}

// 继续执行敏感数据过滤和响应逻辑
if (!isAuthenticated) {
  // ... 原有代码
}

2. 规范checkIfAuthenticated的返回逻辑

确保checkIfAuthenticated仅返回布尔值,不直接发送响应,将响应逻辑移至主代码中,避免分散的响应发送点:

// 修改checkIfAuthenticated实现
async function checkIfAuthenticated(req) {
  // 仅做验证逻辑,不发送响应
  const token = await getToken({ req });
  return !!token && token.valid;
}

// 主代码中处理响应
const isAuthenticated = await checkIfAuthenticated(req);
if (!isAuthenticated) {
  res.status(401).json({ message: '未授权' });
  client.close();
  return;
}

3. 优化数据库连接管理

生产环境建议使用数据库连接池而非每次请求创建/关闭连接,减少资源开销和潜在的异步关闭导致的问题。


内容的提问来源于stack exchange,提问作者Maciej

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 19:41:59