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

Netlify函数运行Commerce.js+SendGrid邮件功能报fetch is not defined错误求助

Commerce.js+SendGrid自定义邮件收据Netlify部署故障修复

问题描述

我尝试使用Commerce.js webhook搭配SendGrid制作自定义邮件收据模板,代码部署到Netlify测试站点,核心代码路径为/functions/email.js。确认.env变量配置正确,但始终未收到邮件收据,查看Netlify函数日志得到如下报错:

5:55:03 PM: f7c003e5 ERROR  Invoke Error    {"errorType":"ReferenceError","errorMessage":"fetch is not defined","stack":["ReferenceError: fetch is not defined","    at Runtime.exports.handler (/var/task/email.js:30:22)","    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"]}
5:55:03 PM: f7c003e5 Duration: 2.24 ms  Memory Usage: 52 MB

现有配置确认

  1. 已在package.json的dependencies中安装node-fetch:
"dependencies": {
    "@chec/commerce.js": "2.2.0",
    "@hookform/error-message": "0.0.5",
    "@sendgrid/mail": "^7.4.7",
    "@stripe/react-stripe-js": "1.1.2",
    "@stripe/stripe-js": "1.11.0",
    "autoprefixer": "10.0.4",
    "classcat": "4.1.0",
    "framer-motion": "2.9.4",
    "next": "10.0.2",
    "next-google-fonts": "1.2.1",
    "node-fetch": "^3.0.0",
    "pdf-lib": "^1.16.0",
    "postcss": "8.1.14",
    "react": "17.0.1",
    "react-dom": "17.0.1",
    "react-hook-form": "6.11.5",
    "react-toastify": "6.1.0",
    "use-debounce": "^7.0.0"
  }
  1. 已按照Netlify函数文档要求,将原本的export default async function handler(req, res) {改为exports.handler = async function(req, res) {,解决了导出语法错误。
  2. 自行配置的请求头如下,不确定是否正确:
let headers = {
    "Accept": "application/json",
    "Content-Type": "application/json",
};

let sgheaders = {
    Authorization: `Bearer ${process.env.SENDGRID_SECRET}`,
    "Content-Type": "application/json",
}; 
  1. 原始代码如下:
// Create the API endpoint function with a req and res parameter
exports.handler = async function(req, res) {

    const checSecretAPIKey = process.env.CHEC_SECRET_KEY;


    let headers = {
        "Accept": "application/json",
        "Content-Type": "application/json",
    };


//export default async function handler(req, res) {
    if (!req.body || req.httpMethod !== 'POST') {
        return {
            status: 405,
            headers,
            body: JSON.stringify({
                status: 'Invalid HTTP method',
            }),
        }
    }

    const { data } = JSON.parse(req.body);

    // Request for your merchant information so that you can use your email
    // to include as the 'from' property to send to the SendGrid API
    const merchant = fetch(`${process.env.CHEC_API_URL}/v1/merchants`, {
        headers: {
            'X-Authoriza†ion': process.env.CHEC_SECRET_KEY,
        },
    }).then((response) => response.json);

    // Extract the signature from the registered `orders.create` webhook
    const { signature } = data;

    delete data.signature;

    // Your Chec webhook signing key, from the Chec Dashboard webhooks view
    const webHookSigningKey = 'KEJlxz6cIlrWIpsX5jypcMeGl2uh7jJg';


    // Verify the signature
    const expectedSignature = crypto.createHmac('sha256', webHookSigningKey)
        .update(JSON.stringify(data))
        .digest('hex');
    if (expectedSignature !== signature) {
        console.error('Signature mismatched, skipping.')
    }

    // Verify the age of the request to make sure it isn't more than 5 minutes old.
    if (new Date(data.created * 1000) < new Date() - 5 * 60 * 1000) {
        console.error('Webhook was sent too long ago, could potentially be fake, ignoring');
    }

    // Because you will need to list out the order line items, map through the returned line items
    // and structure out the data you need to display in the email receipt for your customer
    // Note that we are keeping the data structure minimal here
    const orderLineItems = data.payload.order.line_items.map((lineItem) => ({
        text: lineItem.product_name,
        price: lineItem.line_total.formatted_with_symbol,
    }));

    // Signature is verified, continue to send data to SendGrid
    // Create the email object payload to fire off to SendGrid
    const emailPayload = {
        to: data.payload.customer.email,
        from: merchant.support_email,
        subject: `Thank you for your order ${data.payload.customer.firstname}`,
        text: `Your order number is ${data.payload.customer_reference}`,
        // SendGrid expects a JSON blog containing the dynamic order data your template will use
        // More information below in 'What's next?' on how to configure your dynamic template in SendGrid
        // The property key names will depend on what dynamic template you create in SendGrid
        dynamic_template_data: {
            total: data.payload.order.subtotal.formatted_with_symbol,
            items: orderLineItems,
            receipt: true,
            name: data.payload.shipping.name,
            address01: data.payload.shipping.street,
            city: data.payload.shipping.town_city,
            state: data.payload.shipping.county_state,
            zip : data.payload.shipping.postal_zip_code,
            orderId : data.payload.id,
        },
        // In addition to specifying the dynamic template data, you need to specify the template ID. This comes from your SendGrid dashboard when you create you dynamic template
        template_id: 'd-xxx'
    }


    let sgheaders = {
        Authorization: `Bearer ${process.env.SENDGRID_SECRET}`,
        "Content-Type": "application/json",
    };

    let response = {};
    try {
        // Call the SendGrid send mail endpoint
        response = await sgMailClient.send(emailPayload);
        return {
            statusCode: 200,
            headers: sgheaders,
            body: 'Email sent!'
        }
    } catch (err) {
        console.error('Error', err)
    }
    // Return the response from the request
    return res.status(200).json(response);
}

故障原因

  1. 核心报错fetch is not defined:Netlify函数运行的Node.js版本未内置fetch API,安装的node-fetch@3是ES Module版本,不符合Netlify函数默认的CommonJS规范,且代码中没有手动引入fetch依赖,直接调用全局fetch自然报错。
  2. 其他隐藏问题:
  • 请求商户信息的X-Authorization头拼写错误,混入了特殊字符
  • fetch请求未加await,直接将Promise对象赋值给merchant,无法正确读取商户邮箱
  • 使用SendGrid官方SDK时不需要自行配置请求头,SDK已经内置了鉴权逻辑
  • 部分返回值格式不符合Netlify函数的要求
  • 未引入crypto、SendGrid SDK等必要依赖,也没有对SendGrid SDK做初始化配置

修复后可运行代码

改用CommonJS兼容的axios替代node-fetch,同时适配Commerce.js的返回数据结构,代码如下:

const axios = require('axios');
const sgMailClient = require("@sendgrid/mail");
sgMailClient.setApiKey(process.env.SENDGRID_API_KEY);
// 引入crypto模块
const crypto = require('crypto');


exports.handler = async function(req, res) {
    if (!req.body || req.httpMethod !== 'POST') {
        return {
            status: 405,
            headers: {},
            body: JSON.stringify({
                status: 'Invalid HTTP method',
            }),
        }
    }

    const data = JSON.parse(req.body);

    const { signature } = data;
    delete data.signature;

    const webHookSigningKey = 'KEJlxz6cIlrWIpsX5jypcMeGl2uh7jJg';
    // 校验签名
    const expectedSignature = crypto.createHmac('sha256', webHookSigningKey)
        .update(JSON.stringify(data))
        .digest('hex');
    if (expectedSignature !== signature) {
        console.error('Signature mismatched, skipping.')
    }

    // 校验请求时效,避免伪造请求
    if (new Date(data.created * 1000) < new Date() - 5 * 60 * 1000) {
        console.error('Webhook was sent too long ago, could potentially be fake, ignoring');
    }

    // 处理订单商品列表
    const orderLineItems = data.payload.order.line_items.map((lineItem) => ({
        text: lineItem.product_name,
        price: lineItem.line_total.formatted_with_symbol,
    }));

    // 组装SendGrid邮件参数
    const emailPayload = {
        to: data.payload.customer.email,
        from: data.payload.merchant.support_email,
        subject: `Thank you for your order ${data.payload.customer.firstname}`,
        text: `Your order number is ${data.payload.customer_reference}`,
        dynamic_template_data: {
            total: data.payload.order.subtotal.formatted_with_symbol,
            items: orderLineItems,
            receipt: true,
            name: data.payload.billing.name,
            address01: data.payload.billing.street,
            city: data.payload.billing.town_city,
            state: data.payload.billing.county_state,
            zip : data.payload.billing.postal_zip_code,
            orderId : data.payload.id,
        },
        template_id: 'd-xxx' // 替换为自己的SendGrid模板ID
    };

    let response = {};
    try {
        response = await sgMailClient.send(emailPayload);
        return {
            statusCode: 200,
            headers: {},
            body: JSON.stringify({
                status: 'Email sent!',
            }),
        }
    } catch (err) {
        console.error('Error from function: ', err)
        console.error(err.response.body);
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 00:36:02