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

使用Nodemailer发送注册OTP时遭遇ERR_HTTP_HEADERS_SENT错误求助

Express注册发送OTP邮件时ERR_HTTP_HEADERS_SENT错误排查与修复

在Express中实现用户注册并通过Nodemailer发送OTP验证邮件时,触发Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client错误,核心原因是多次向客户端发送HTTP响应,同时代码还存在参数传递错误、时间计算错误等问题。

错误原因分析

  • 混合使用async/await与.then()导致重复响应:在newUser.save().then()回调中,既调用了会发送响应的sendOTPVerificationEmail,又执行return res.status(201).json(...),之后路由末尾还有另一个return res.status(201).json(...),一次请求内多次调用res.send/res.json触发错误。
  • sendOTPVerificationEmail参数错误:函数定义接收(req, res),但实际传入的是(newUser, res),且函数内部直接使用未定义的email、_id变量,触发catch分支的响应,与路由内的响应冲突。
  • OTP过期时间计算错误:expiresAt: Date.now() * 3600000是将当前时间戳乘以3600000,逻辑错误,应改为Date.now() + 3600000(当前时间加1小时)。

修复后的代码

注册路由

app.post("/signup", async (req, res) => {
    const { username, email, password, cnfrmPass } = req.body;
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{8,}$/;
    
    if (!passwordRegex.test(password)) {
        return res.status(400).json({ error: '密码不符合要求' });
    }

    try {
        const existingUser = await userModel.findOne({ email });
        if (existingUser) {
            return res.status(400).json({ error: '用户已存在' });
        }

        const newUser = new userModel({ username, email, password });
        const savedUser = await newUser.save();
        
        // 交由OTP函数统一处理响应
        await sendOTPVerificationEmail(savedUser, res);
    } catch (error) {
        console.error('错误:', error);
        // 避免重复发送错误响应
        if (!res.headersSent) {
            res.status(500).json({ error: '服务器内部错误' });
        }
    }
})

OTP发送函数

const sendOTPVerificationEmail = async (user, res) => {
    try {
        const { _id, email } = user;
        const otp = `${Math.floor(1000 + Math.random() * 9000)}`;
        const mailOptions = {
            from: process.env.AUTH_EMAIL,
            to: email,
            subject: "验证你的邮箱",
            html: `<p>在应用中输入 <b>${otp}</b> 以验证邮箱并完成注册</p>
            <p>此验证码 <b>1小时后过期</b>。</p>`
        };

        const saltRounds = 10;
        const hashedOTP = await bcrypt.hash(otp, saltRounds);
        const newOTPVerification = new UserOTPVerification({
            userId: _id,
            otp: hashedOTP,
            createdAt: Date.now(),
            expiresAt: Date.now() + 3600000, // 当前时间加1小时
        });
        
        await newOTPVerification.save();
        await transporter.sendMail(mailOptions);
        
        res.status(201).json({
            status: "PENDING",
            message: "验证邮件已发送",
            data: {
                userId: _id,
                email,
            }
        });
    } catch (error) {
        console.error('发送OTP错误:', error);
        res.status(500).json({
            status: "FAILED",
            message: "发送验证邮件失败",
            error: error.message
        });
    }
}

关键修复点

  • 统一使用async/await替代混合写法,避免回调嵌套导致的响应重复发送
  • 调整响应逻辑:由sendOTPVerificationEmail统一处理成功/失败的响应,路由不再额外发送响应
  • 修正sendOTPVerificationEmail的参数,从传入的user对象中提取_id和email
  • 修复OTP过期时间的计算逻辑
  • 在catch分支中增加res.headersSent判断,避免重复发送错误响应

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 09:34:58