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

NextAuth集成nodemailer后sendVerificationRequest不触发无报错

NextAuth自定义邮件模板后sendVerificationRequest不触发问题排查

问题描述

  • 基于NextAuth实现邮箱魔术链接登录:未接入nodemailer、Handlebars自定义邮件模板时,默认邮件发送逻辑运行正常
  • 接入二者替换自定义模板后,点击signIn按钮时程序不会进入sendVerificationRequest函数执行,全程无任何报错抛出
  • 问题对应的[...nextauth].js配置代码如下:
import NextAuth from "next-auth/next";
import EmailProvider from "next-auth/providers/email";
import nodemailer from 'nodemailer'
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";
import Handlebars from 'handlebars'
import { readFileSync } from 'fs'
import path from "path";

const prisma = new PrismaClient()

const transporter = nodemailer.createTransport({
    host: process.env.EMAIL_SERVER_HOST,
    port: process.env.EMAIL_SERVER_PORT,
    auth: {
        user: process.env.EMAIL_SERVER_USER,
        pass: process.env.EMAIL_SERVER_PASSWORD,
    },
    secure: true,
});

const emailsDir = path.resolve(process.cwd(), 'emails');

const sendVerificationRequest = async ({ identifier, url }) => {
    console.log('send verification req entered')
    const emailFile = readFileSync(path.join(emailsDir, 'confirm-email.html'), {
        encoding: 'utf8',
    });
    const emailTemplate = Handlebars.compile(emailFile);
    const result = await transporter.sendMail({
        from: `"✨ FreiDrops" ${process.env.EMAIL_FROM}`,
        to: identifier,
        subject: 'Your sign-in link for FreiDrops',
        html: emailTemplate({
            base_url: process.env.NEXTAUTH_URL,
            signin_url: url,
            email: identifier,
        }),
    });
    const failed = result.rejected.concat(result.pending).filter(Boolean)
    if (failed.length) {
        throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
    }
};
export default NextAuth({
    pages: {
        signIn: '/',
        signOut: '/',
        error: '/',
        verifyRequest: '/'
    },
    providers: [
        EmailProvider({
            maxAge: 10 * 60, // Magic links are valid for 10min!!
            sendVerificationRequest,
        })
    ],
    adapter: PrismaAdapter(prisma)
})

配置错误点与修复方案

你的配置确实存在3个会直接导致该问题的错误,逐一修复即可:

  • EmailProvider必填配置缺失
    未自定义发送函数时,NextAuth内置邮件逻辑会自动读取环境变量里的邮件服务、发件人配置完成初始化;你传入自定义sendVerificationRequest后,Provider不会再自动读取这些环境变量,缺失server和from必填项时会直接静默初始化失败,根本不会触发你写的发送函数。
    修复方式:在EmailProvider配置中补全必填字段:
    EmailProvider({
        maxAge: 10 * 60, // 魔术链接10分钟有效期
        server: {
            host: process.env.EMAIL_SERVER_HOST,
            port: Number(process.env.EMAIL_SERVER_PORT),
            auth: {
                user: process.env.EMAIL_SERVER_USER,
                pass: process.env.EMAIL_SERVER_PASSWORD,
            },
            secure: Number(process.env.EMAIL_SERVER_PORT) === 465, // 465端口走SSL设为true,587等STARTTLS端口设为false
        },
        from: process.env.EMAIL_FROM,
        sendVerificationRequest,
    })
    
  • 顶层同步操作错误被静默吞掉
    你把模板目录路径解析、transporter初始化的逻辑都放在了API路由模块的顶层,这部分代码会在NextAuth路由初始化时执行。如果模板文件路径不对、文件不存在,或者nodemailer连接参数不匹配,抛出的同步错误会被Next.js API路由边界捕获后静默吞掉,直接导致整个NextAuth配置加载失败,点击登录不会有任何响应。
    修复方式:将非全局必要的逻辑挪到sendVerificationRequest函数内部执行,同时提前确认emails文件夹会被正确打包到生产构建产物中,避免生产环境找不到文件:
    const sendVerificationRequest = async ({ identifier, url }) => {
        console.log('send verification req entered')
        // 把路径解析、模板读取挪到函数内部
        const emailsDir = path.resolve(process.cwd(), 'emails');
        const emailFile = readFileSync(path.join(emailsDir, 'confirm-email.html'), {
            encoding: 'utf8',
        });
        const emailTemplate = Handlebars.compile(emailFile);
        const result = await transporter.sendMail({
            from: `"✨ FreiDrops" ${process.env.EMAIL_FROM}`,
            to: identifier,
            subject: 'Your sign-in link for FreiDrops',
            html: emailTemplate({
                base_url: process.env.NEXTAUTH_URL,
                signin_url: url,
                email: identifier,
            }),
        });
        const failed = result.rejected.concat(result.pending).filter(Boolean)
        if (failed.length) {
            throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
        }
    };
    
  • 端口参数类型错误
    从环境变量读取的EMAIL_SERVER_PORT默认是字符串类型,nodemailer要求port字段为数字类型,类型不匹配会导致transporter初始化失败,错误同样会被静默吞掉。修复方式参考上面的代码,读取端口时用Number()做类型转换即可。

修复完成后重启服务,点击登录按钮就能看到你写的入口日志正常打印,后续再调试模板渲染、邮件发送的逻辑即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 01:15:29