部署至DigitalOcean后注册接口异常,请求排查连接超时问题
部署到DigitalOcean后注册接口发送邮件超时问题排查
问题概况
注册服务端接口在本地运行正常,但部署到DigitalOcean后出现异常,即使在服务器上以开发模式运行也无法解决。收到如下超时错误:
Error in registerUser: [Error: Connection timeout] { code: 'ETIMEDOUT', command: 'CONN' }
数据库能正常更新,推测问题出在发送邮件环节。已尝试开放TCP 587、465端口,怀疑过环境变量未加载,但本地npm可自动加载,服务器用npm测试理论上应生效。
相关代码
注册服务端接口代码
"use server"; import { actionClient } from "@/lib/safe-action"; import { registerSchema } from "@/schemas/Register-schema"; import { prisma } from "@/prisma"; import bcrypt from "bcryptjs"; import crypto from "crypto"; import { sendSMS } from "@/lib/sms"; import { sendEmail } from "@/lib/email"; export const registerUser = actionClient .schema(registerSchema) .action( async ({ parsedInput: { forename, surname, email, mobile, password, confirmPassword, role, }, }) => { try { const emailLower = email.toLowerCase(); if (password !== confirmPassword) { throw new Error("Passwords do not match."); } const existingUser = await prisma.user.findFirst({ where: { email: emailLower, }, }); if (existingUser) { return { error: "User already exists" }; } const salt = bcrypt.genSaltSync(10); const pwHash = bcrypt.hashSync(password, salt); const expires = new Date(); expires.setHours(expires.getHours() + 6); const token = crypto.randomBytes(32).toString("hex"); await prisma.user.create({ data: { forename, surname, email: emailLower, mobile, role, password: pwHash, }, }); await prisma.emailToken.create({ data: { token, expires, user: { connect: { email: emailLower, }, }, }, }); const mobileToken = ( Math.floor(Math.random() * (999999 - 100000 + 1)) + 100000 ).toString(); const mobileExpires = new Date(); mobileExpires.setHours(mobileExpires.getHours() + 6); await prisma.mobileToken.create({ data: { token: mobileToken, expires: mobileExpires, user: { connect: { mobile, }, }, }, }); await sendEmail( email, "welcome@tutacall.com", "Verify your email", `Visit ${process.env.BASE_URL}/verify-email?token=${token} to verify your email address.`, `<a href="${process.env.BASE_URL}/verify-email?token=${token}">Verify your email</a>` ); await sendSMS( mobile, "Tutacall", `Your verification code is: ${mobileToken}. Login and use it within 6 hours.` ); return { success: "Verification Email and SMS sent!" }; } catch (error) { console.error("Error in registerUser:", error); return { error: "An error occurred. Please try again." }; } } );
发送邮件函数代码
import nodemailer from "nodemailer"; interface MailData { from: string; replyTo: string; to: string; subject: string; text: string; html: string; } export const sendEmail = async ( email: string, from: string, subject: string, text: string, html: string ) => { const transporter = nodemailer.createTransport({ host: "smtp.resend.com", port: 587, auth: { user: "resend", pass: process.env.RESEND_API_KEY, }, }); await new Promise<void>((resolve, reject) => { // verify connection configuration transporter.verify(function (error) { if (error) { console.log(error.message); reject(error); } else { console.log("Server is ready to take our messages"); resolve(); } }); }); const mailData: MailData = { from: from, replyTo: from, to: email, subject: subject, text: text, html: html, }; await new Promise<void>((resolve, reject) => { // send mail transporter.sendMail( mailData, (err: Error | null, info: nodemailer.SentMessageInfo) => { if (err) { console.error(err); reject(err); } else { console.log(info); resolve(info); } } ); }); };
排查建议
- 确认环境变量加载情况
- 服务器上直接打印
process.env.RESEND_API_KEY,检查是否有值。注意Next.js生产环境需用.env.production,.env.local仅本地开发生效;如果用DigitalOcean App Platform,需在平台配置里手动添加环境变量,检查拼写是否正确。
- 服务器上直接打印
- 测试服务器出站网络
- 在服务器上执行
telnet smtp.resend.com 587,看是否能建立连接;如果是Droplet,用sudo ufw status检查防火墙是否允许出站587/465端口。
- 在服务器上执行
- 调整Nodemailer配置
- 尝试明确配置TLS:
const transporter = nodemailer.createTransport({ host: "smtp.resend.com", port: 587, secure: false, auth: { user: "resend", pass: process.env.RESEND_API_KEY, }, tls: { rejectUnauthorized: false } // 仅测试用,生产环境需移除 }); - 切换到465端口并开启
secure: true:const transporter = nodemailer.createTransport({ host: "smtp.resend.com", port: 465, secure: true, auth: { user: "resend", pass: process.env.RESEND_API_KEY, } });
- 尝试明确配置TLS:
- 检查Resend服务状态
- 登录Resend控制台,确认API密钥有效、未被禁用,查看是否有服务器IP的请求被拒绝的日志。
- 细化错误捕获
- 在
sendEmail函数中分别捕获连接验证和发送邮件的错误,打印更详细日志,明确失败环节:try { await new Promise<void>((resolve, reject) => { transporter.verify(function (error) { if (error) { console.log("Verify connection error:", error); reject(error); } else { console.log("Server is ready to take our messages"); resolve(); } }); }); } catch (verifyErr) { console.error("Verify failed:", verifyErr); throw verifyErr; } try { await new Promise<void>((resolve, reject) => { transporter.sendMail(mailData, (err, info) => { if (err) { console.log("Send mail error:", err); reject(err); } else { console.log("Mail sent:", info); resolve(info); } }); }); } catch (sendErr) { console.error("Send failed:", sendErr); throw sendErr; }
- 在
内容的提问来源于stack exchange,提问作者sam
相关产品推荐
相关产品推荐

