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

Next.js中用SendGrid实现表单发邮件遇401未授权错误求助

解决Next.js + SendGrid 401未授权错误

问题场景

我在Next.js中创建一个报价请求表单,使用SendGrid作为第三方API发送邮件/提交表单报价请求,但一直收到401未授权错误,无法将表单与邮件功能关联。

错误信息

error - ResponseError: Unauthorized
    at node_modules/@sendgrid/client/src/classes/client.js:146:29
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  code: 401,
  response: {
    headers: {
      server: 'nginx',
      date: 'Wed, 18 Jan 2023 17:25:46 GMT',
      'content-type': 'application/json',
      'content-length': '116',
      connection: 'close',
      'access-control-allow-origin': 'https://sendgrid.api-docs.io',
      'access-control-allow-methods': 'POST',
      'access-control-allow-headers': 'Authorization, Content-Type, On-behalf-of, x-sg-elas-acl',
      'access-control-max-age': '600',
      'x-no-cors-reason': 'https://sendgrid.com/docs/Classroom/Basics/API/cors.html',
      'strict-transport-security': 'max-age=600; includeSubDomains'
    },
    body: { errors: [Array] }
  },
  page: '/api/contact'
}

相关代码

pages/contact.jsx

const contact = () => {
  const [values, setValues] = useState({
    firstName: "",
    lastName: "",
    phone: "",
    email: "",
    zip: "",
    message: "",
  });

  const {firstName, lastName, phone, email, zip, message} = values;

  const handleChange = e => 
    setValues({ ...values, [e.target.name]: e.target.value });

  const handleSubmit = async (e) => {
    e.preventDefault()
    try {
      await fetch("http://localhost:3000/api/contact", {
        method: "POST",
        header: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(values),
      });
    } catch (err) {
      console.log(err);
    }
  };

 return (
    <div className='w-full h-screen'>
      <div className='min-h-screen py-40 border border-gray-800 bg-white bg-gradient-to-br from-sjrblue to-sjrblue-200'>
        <div className="container mx-auto">
          <div className="flex flex-col lg:flex-row w-10/12 bg-white rounded-xl mx-auto shadow-lg overflow-hidden">
            <div alt='loaded trailer' className='w-full h-[400px] lg:gap-5 lg:h-auto lg:w-1/2 bg-contactImage bg-cover bg-center text-center shadow-lg shadow-slate-400'></div>

            {/* Request Quote Form */}
            <div className="lg:w-1/2 lg:m-2 p-4 items-center">
              <h2 className='text-3xl mb-4 text-center text-sjrblue'>Company Name</h2>
              <p className='mb-4 text-center'>CONTACT US TODAY FOR A <span className='text-blue font-bold'>FREE</span> JUNK REMOVAL ESTIMATE</p>

              <form onSubmit={handleSubmit}>
                <div className='grid grid-cols-2 gap-1 md:gap-3 lg:gap-5'>
                  <input 
                    type="text" 
                    placeholder='first name' 
                    value={firstName} 
                    onChange={handleChange}  
                    required
                    name='firstName' 
                    className='border border-gray-400 py-1 px-2 shadow-lg shadow-slate-400'
                  />
                  <input 
                    type="text" 
                    placeholder='sur name'
                    value={lastName}
                    onChange={handleChange} 
                    required 
                    name='lastName'
                    className='border border-gray-400 py-1 px-2 shadow-lg shadow-slate-400'
                  />
                </div>
                <div className='mt-5'>
                  <input 
                    type="email" 
                    placeholder='email'
                    value={email}
                    onChange={handleChange} 
                    required 
                    name='email' 
                    className='text-sm border border-gray-400 py-1 px-2 w-full shadow-lg shadow-slate-400'
                  />
                </div>
                <div className='mt-5'>
                  <input 
                    type="tel" 
                    id='phone-number' 
                    onkeydown='phoneNumberFormatter()' 
                    inputmode='tel' 
                    placeholder='phone number 123-456-7890' 
                    pattern='[0-9]{3}-[0-9]{3}-[0-9]{4}' 
                    value={phone}
                    onChange={handleChange} 
                    required 
                    name='phone' 
                    className='text-sm border border-gray-400 py-1 px-2 w-full shadow-lg shadow-slate-400'
                  />
                </div>
                <div className='mt-5'>
                  <input 
                    type="text" 
                    inputmode='numeric' 
                    placeholder='zip code' 
                    maxLength={5}
                    value={zip}
                    onChange={handleChange} 
                    name='zip' 
                    required 
                    className='text-sm border border-gray-400 py-1 px-2 w-full shadow-lg shadow-slate-400'
                  />
                </div>
                <div className='mt-5'>
                  <textarea 
                    name="message" 
                    placeholder='message' 
                    id="" cols="30" 
                    rows="10" 
                    value={message}
                    onChange={handleChange}  
                    required 
                    className='text-sm border border-gray-400 py-1 px-2 w-full shadow-lg shadow-slate-400'>  
                  </textarea>
                </div>
                <div className='mt-5 text-center'>
                  <input type="checkbox" required className='border border-gray-400 w-3 h-3'/>
                  <span className='px-3 text-sm'>I am not a robot</span>
                  <div className='mt-5'>
                    <button className='bg-sjrblue w-full text-enter text-white py-3 rounded  hover:bg-blue-700  duration-300 shadow-lg shadow-slate-400'>Get your FREE estimate</button>
                  </div>
                </div>
              </form>
            </div>

          </div>
        </div>
      </div>  
);
};

export default contact;

pages/api/contact.js

require("dotenv").config();
const sgMail = require("@sendgrid/mail");

const { SG_API_KEY, FROM_EMAIL, TO_EMAIL } = process.env;
sgMail.setApiKey(SG_API_KEY);

export default async function handler(req, res) {
    const {firstName, lastName, phone, email, zip, message} = req.body;

    const msg = {
        to: TO_EMAIL,
        from: FROM_EMAIL,
        subject: '',
        html: `<p><strong>First Name: </strong>${firstName}</p>
        <p><strong>Last Name: </strong>${lastName}</p>
        <p><strong>Phone Number: </strong>${phone}</p>
        <p><strong>Email: </strong>${email}</p>
        <p><strong>Zip: </strong>${zip}</p>
        <p><strong>Message: </strong>${message}</p>
        `,
    };
    await sgMail.send(msg);
    console.log('email sent');
    res.status(200).json({ success: true  });
}

根目录.env文件

SG_API_KEY= #API Key
TO_EMAIL=  #verified email
FROM_EMAIL= #email to send form to

排查与解决步骤

  • 验证API密钥正确性
    确保.env中的SG_API_KEY是从SendGrid控制台生成的完整密钥,无多余空格或换行。创建密钥时需勾选「Mail Send」权限,或直接选择「Full Access」。

  • 确认环境变量加载正常
    在pages/api/contact.js中添加console.log(SG_API_KEY),启动项目后查看控制台输出:

    • 若输出undefined,检查.env是否在项目根目录,文件名是否为.env(注意开头的点)
    • 修改.env后需重启Next.js开发服务器,环境变量才会生效
    • 此API路由无需NEXT_PUBLIC_前缀,直接使用变量名即可
  • 验证邮箱合法性
    SendGrid要求FROM_EMAIL必须是已验证的邮箱或域名,免费版用户的TO_EMAIL也需要提前验证,否则会触发权限拦截。

  • 修复代码细节问题

    1. 前端fetch请求修正:header拼写错误改为headers,使用相对路径避免环境差异:
      await fetch("/api/contact", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(values),
      });
      
    2. 后端添加错误捕获:避免SendGrid报错导致API路由崩溃,同时输出详细错误信息:
      export default async function handler(req, res) {
        try {
          const {firstName, lastName, phone, email, zip, message} = req.body;
          const msg = {
            to: TO_EMAIL,
            from: FROM_EMAIL,
            subject: `新报价请求:${firstName} ${lastName}`,
            html: `<p><strong>First Name: </strong>${firstName}</p>
            <p><strong>Last Name: </strong>${lastName}</p>
            <p><strong>Phone Number: </strong>${phone}</p>
            <p><strong>Email: </strong>${email}</p>
            <p><strong>Zip: </strong>${zip}</p>
            <p><strong>Message: </strong>${message}</p>
            `,
          };
          await sgMail.send(msg);
          console.log('email sent');
          res.status(200).json({ success: true });
        } catch (error) {
          console.error('SendGrid错误详情:', error.response?.body?.errors || error.message);
          res.status(error.code || 500).json({ success: false, error: error.message });
        }
      }
      
    3. .env文件格式修正:注释与变量值分开,避免解析错误:
      # SendGrid API密钥
      SG_API_KEY=你的完整API密钥
      # 收件邮箱(需验证)
      TO_EMAIL=xxx@example.com
      # 发件邮箱(需验证)
      FROM_EMAIL=yyy@example.com
      

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 14:01:30