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

部署至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,
        }
      });
      
  • 检查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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 10:54:55