使用jsonwebtoken生成Token时出现ERR_HTTP_HEADERS_SENT错误求助
问题:MERN项目中用户注册/登录时JWT生成触发"Cannot set headers after they are sent to the client"错误
错误日志
throw er; // Unhandled 'error' event ^ Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client at new NodeError (node:internal/errors:393:5) at ServerResponse.setHeader (node:_http_outgoing:644:11) at ServerResponse.header (D:\web dev\projects\Mern-Project\server\node_modules\express\lib\response.js:794:10) at ServerResponse.send (D:\web dev\projects\Mern-Project\server\node_modules\express\lib\response.js:174:12) at ServerResponse.json (D:\web dev\projects\Mern-Project\server\node_modules\express\lib\response.js:278:15) at D:\web dev\projects\Mern-Project\server\router\auth.js:45:34 at D:\web dev\projects\Mern-Project\server\node_modules\mongoose\lib\model.js:5228:18 at process.processTicksAndRejections (node:internal/process/task_queues:77:11) Emitted 'error' event on Function instance at: at D:\web dev\projects\Mern-Project\server\node_modules\mongoose\lib\model.js:5230:15 at process.processTicksAndRejections (node:internal/process/task_queues:77:11) { code: 'ERR_HTTP_HEADERS_SENT' }
相关代码
Auth路由代码
const express = require("express"); const { default: mongoose } = require("mongoose"); const router = express.Router(); const bcrypt = require("bcrypt"); const jwt = require("jsonwebtoken"); require("../db/conn"); const User = require("../model/userSchema"); router.get("/", (req, res)=>{ res.send("Hello world to server router js"); }); router.post("/register", (req, res)=>{ bcrypt.hash(req.body.password, 12, function (err, hash) { const newUser = new User({ name:req.body.name, email: req.body.email, phone: req.body.phone, work: req.body.work, password: hash }); // Create token const email = req.body.email; const token = jwt.sign( { user_id: newUser._id, email }, process.env.TOKEN_KEY, { expiresIn: "2h", } ); // save user token newUser.token = token; res.status(201).json(newUser); if(!newUser.name || !newUser.email || !newUser.phone || ! newUser.work || !newUser.password){ return res.status(422).json({error: "Plz fill the required field"}); } User.findOne({email: newUser.email}, function (err, userExist) { if(err){ console.log(err); res.status(500).json({error: "failed to register"}); }else{ if(userExist){ return res.status(422).json({error: "Email already Exist"}); }else{ newUser.save(); res.status(201).json({message: "user registered succesfully"}); } } }); }); }); //LOgin route router.post("/signin", (req, res)=>{ if(!req.body.email || !req.body.password){ return res.status(400).json({error: "Plz fill the required data"}); }else{ bcrypt.hash(req.body.password, 12, function (err, hash) { User.findOne({email: req.body.email}, function (err, foundUser) { if(err){ console.log(err); }else{ if(foundUser){ bcrypt.compare(req.body.password, foundUser.password, function (err, result) { if(result){ res.json({message: "successfully log in"}) }else{ res.json({message: "incorrect password"}); } }); const email = req.body.email; const token = jwt.sign( { user_id: foundUser._id, email }, process.env.TOKEN_KEY, { expiresIn: "2h", } ); foundUser.token = token; res.status(200).json(foundUser); }else{ res.status(400).json({message: "user not found"}); }; } }) }) } }); module.exports = router;
用户Schema与模型代码
const mongoose = require("mongoose"); const userSchema = new mongoose.Schema({ name:{ type:String, required:true }, email:{ type:String, required:true }, phone:{ type:Number, required:true }, work:{ type:String, required:true }, password:{ type:String, required:true }, tokens:[ { token:{ type:String, required:true } }] }); const User = mongoose.model("User", userSchema); module.exports = User;
问题原因分析
- 多次发送HTTP响应:Express每个请求仅允许发送一次响应,你的代码存在多处重复响应逻辑:
- 注册路由:先执行
res.status(201).json(newUser),后续又执行字段校验、邮箱查重的响应操作,导致同一请求多次发送响应头。 - 登录路由:
bcrypt.compare回调中已发送res.json({message: ...}),后续又执行res.status(200).json(foundUser),重复响应。
- 注册路由:先执行
- Token存储错误:Schema中
tokens是数组类型,但代码直接赋值newUser.token = token,不符合Schema定义,需将Token添加到数组中。 - 逻辑顺序颠倒:注册路由先生成Token并返回响应,再做字段校验和邮箱查重,业务逻辑顺序完全错误——应先校验,通过后再生成Token并保存用户。
- 登录冗余操作:登录时无需重新哈希密码,
bcrypt.hash(req.body.password, 12, ...)是多余操作,直接用原密码和数据库哈希值对比即可。
修复后的代码示例
修复后的Auth路由
const express = require("express"); const router = express.Router(); const bcrypt = require("bcrypt"); const jwt = require("jsonwebtoken"); require("../db/conn"); const User = require("../model/userSchema"); router.get("/", (req, res) => { res.send("Hello world to server router js"); }); // 注册路由修复 router.post("/register", async (req, res) => { try { // 先做字段校验 const { name, email, phone, work, password } = req.body; if (!name || !email || !phone || !work || !password) { return res.status(422).json({ error: "请填写所有必填字段" }); } // 检查邮箱是否已存在 const userExist = await User.findOne({ email }); if (userExist) { return res.status(422).json({ error: "邮箱已存在" }); } // 哈希密码 const hash = await bcrypt.hash(password, 12); const newUser = new User({ name, email, phone, work, password: hash }); // 生成Token const token = jwt.sign( { user_id: newUser._id, email }, process.env.TOKEN_KEY, { expiresIn: "2h" } ); // 将Token添加到tokens数组 newUser.tokens.push({ token }); // 保存用户到数据库 await newUser.save(); res.status(201).json({ message: "用户注册成功", user: newUser }); } catch (err) { console.log(err); res.status(500).json({ error: "注册失败" }); } }); // 登录路由修复 router.post("/signin", async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: "请填写邮箱和密码" }); } // 查询用户 const foundUser = await User.findOne({ email }); if (!foundUser) { return res.status(400).json({ message: "用户不存在" }); } // 密码校验 const isMatch = await bcrypt.compare(password, foundUser.password); if (!isMatch) { return res.status(400).json({ message: "密码错误" }); } // 生成Token const token = jwt.sign( { user_id: foundUser._id, email }, process.env.TOKEN_KEY, { expiresIn: "2h" } ); // 添加Token到用户的tokens数组并保存 foundUser.tokens.push({ token }); await foundUser.save(); res.status(200).json({ message: "登录成功", user: foundUser, token }); } catch (err) { console.log(err); res.status(500).json({ error: "登录失败" }); } }); module.exports = router;
注意事项
- 改用
async/await替代回调函数,避免嵌套逻辑混乱,代码可读性更强。 - 确保每个请求仅发送一次响应,所有分支逻辑通过
return终止后续代码执行。 - 严格按照Schema定义操作数据,数组类型字段用
push添加元素。 - 登录流程无需重复哈希密码,直接使用
bcrypt.compare对比原密码与数据库哈希值。
内容的提问来源于stack exchange,提问作者Jatin
相关产品推荐
相关产品推荐

