Next.js单个API路由生产环境500错误排查求助
问题背景
仅/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

