晚上修 webhook 最怕这种情况:wrangler dev 本地一切正常,preview URL 也能调通,合并到 main 后生产环境开始返回 401、500,日志里只有一句 missing API key。代码看起来没动,密钥也明明在本机文件里,真正出错的位置通常在环境选择和 secret 名称。

Cloudflare Workers 的 secret 可以像环境变量一样从 env 里读,但它不是一份全局配置。Local preview、Wrangler environment、已部署 Worker 和 GitHub Actions 是四个入口。排查时不要先改业务逻辑,先把这四处对齐。

preview、production、environment 到底差在哪?

先把词分开,很多误判都来自把 preview 当成 production 的影子。

场景常见命令或入口secret 来源最容易错在哪里
本地开发npx wrangler dev.dev.vars.env本地有值,线上没有值
本地指定环境npx wrangler dev --env preview.dev.vars.preview.env.preview文件名存在后,加载顺序和默认文件不同
预览部署npx wrangler deploy --env previewpreview environment 对应 Worker 的 secret写到了顶层 Worker,preview Worker 读不到
生产部署npx wrangler deploy --env production 或不带 --env 的顶层部署production environment 或顶层 Worker 的 secret团队对「production」到底是 env 还是顶层 Worker 没约定

Cloudflare 的 Wrangler environment 会让同一个项目拥有不同配置;例如顶层 name 是 my-apienv.preview 可能部署成 my-api-preview。这意味着你不是给「一个 Worker 的两个模式」塞 secret,而是在给不同 Worker 分别配置 secret。

快速判断:代码问题还是配置问题?

先找一个只读的健康检查,不要用真实支付、发信或扣费接口验证。比如给 Hono、itty-router 或原生 Worker 临时加一个只返回状态的路径,确认 secret 名称存在,不返回 secret 值。

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/__health/secrets") {
      return Response.json({
        hasStripeKey: Boolean(env.STRIPE_SECRET_KEY),
        hasWebhookSecret: Boolean(env.STRIPE_WEBHOOK_SECRET),
        runtime: env.APP_ENV ?? "unknown"
      });
    }

    return new Response("ok");
  }
};

如果 preview 返回 true、production 返回 false,代码路径大概率没坏,问题在 secret 来源。如果两边都是 false,再看 TypeScript 类型、binding 名称和 wrangler.jsonc 里的 secrets.required。不要把 secret 值打印到日志里,日志系统、Sentry、Axiom 或 PostHog 都可能把它保存下来。

secret 名称要怎么逐个对齐?

先固定一组名字,别同时出现 OPENAI_KEYOPENAI_API_KEYOPENAI_SECRET。独立开发者项目里,我会把第三方平台、用途和环境无关信息写进名称,环境差异只体现在值上。

STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
RESEND_API_KEY
OPENAI_API_KEY
DATABASE_URL

然后按同一个名字检查四处:

# 本地 preview 文件
grep -n "STRIPE_SECRET_KEY" .dev.vars .dev.vars.preview .env .env.preview 2>/dev/null

# 本地用 preview 环境跑
npx wrangler dev --env preview

# 给 preview environment 写 secret
npx wrangler secret put STRIPE_SECRET_KEY --env preview

# 给 production environment 写 secret
npx wrangler secret put STRIPE_SECRET_KEY --env production

如果你的生产就是顶层 Worker,没有在 wrangler.jsonc 里定义 env.production,生产 secret 命令应是不带环境的:

npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler deploy

如果团队已经定义了 env.production,那就统一带上环境:

npx wrangler secret put STRIPE_SECRET_KEY --env production
npx wrangler deploy --env production

两种方式不要混着用。混用后最常见的现场是:Dashboard 里顶层 Worker 有 secret,my-api-production 没有;或者 CI 一直部署 --env production,你却在本机给顶层 Worker put 了 secret。

wrangler.jsonc 里哪些字段不能指望继承?

Cloudflare 文档把 bindings、环境变量和 secrets 归到 environment 级别;这些配置不能靠「顶层有一份」就默认带到每个 environment。vars 也是同理,env.production.vars 需要自己写。

一个更稳的 wrangler.jsonc 可以这样收口:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "my-saas-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-03-20",
  "vars": {
    "APP_ENV": "preview",
    "API_HOST": "https://api-preview.example.com"
  },
  "secrets": {
    "required": [
      "STRIPE_SECRET_KEY",
      "STRIPE_WEBHOOK_SECRET",
      "RESEND_API_KEY"
    ]
  },
  "env": {
    "production": {
      "name": "my-saas-api-production",
      "vars": {
        "APP_ENV": "production",
        "API_HOST": "https://api.example.com"
      },
      "routes": [
        {
          "pattern": "api.example.com/*",
          "zone_name": "example.com"
        }
      ],
      "secrets": {
        "required": [
          "STRIPE_SECRET_KEY",
          "STRIPE_WEBHOOK_SECRET",
          "RESEND_API_KEY"
        ]
      }
    },
    "preview": {
      "name": "my-saas-api-preview",
      "vars": {
        "APP_ENV": "preview",
        "API_HOST": "https://api-preview.example.com"
      },
      "secrets": {
        "required": [
          "STRIPE_SECRET_KEY",
          "STRIPE_WEBHOOK_SECRET",
          "RESEND_API_KEY"
        ]
      }
    }
  }
}

这里有三个取舍。第一,vars 只放不敏感配置,API key、数据库密码、webhook signing secret 都走 secret。第二,APP_ENV 可以是普通变量,因为它只是帮助日志和健康检查判断当前跑在哪个环境,不是凭据。第三,secrets.required 对 named environment 不能只依赖顶层配置,env.productionenv.preview 里也要写清必需 secret 名称。

GitHub Actions 部署前怎么验生产 secret?

CI 里最怕「deploy 成功,但生产第一次真实请求才发现没 secret」。可以在 workflow 里把部署和 smoke test 分开。smoke test 只测是否能读到必需名字,不调用真实扣费链路。

name: Deploy Worker

on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx wrangler deploy --env production
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
      - run: |
          curl --fail --silent --show-error \
            https://api.example.com/__health/secrets

如果你要在 CI 同步上传 secret 文件,Cloudflare 支持 --secrets-file,但这会把密钥管理压力转到 GitHub secret、环境审批和日志脱敏上。小团队更常见的做法是:密钥轮换时手动 wrangler secret put 到目标 environment,日常 CI 只负责部署代码。

跨地区维护 Cloudflare Dashboard、GitHub Actions 日志和 Wrangler 登录时,最烦的是偶发加载失败混进真实配置问题。需要远程处理生产事故时,可以准备一条 海外服务跑 GitHub Actions / Cloudflare 的稳定线路,但它只能减少排查入口抖动,不能替代 secret 权限、环境命名和部署验证。

改完后用哪些命令验收?

先在本地确认 preview 环境读到的名称:

npx wrangler dev --env preview
curl http://localhost:8787/__health/secrets

再部署 preview,不碰生产流量:

npx wrangler secret put STRIPE_SECRET_KEY --env preview
npx wrangler secret put STRIPE_WEBHOOK_SECRET --env preview
npx wrangler deploy --env preview
curl https://my-saas-api-preview.<your-subdomain>.workers.dev/__health/secrets

最后处理 production:

npx wrangler secret put STRIPE_SECRET_KEY --env production
npx wrangler secret put STRIPE_WEBHOOK_SECRET --env production
npx wrangler deploy --env production
curl --fail https://api.example.com/__health/secrets

如果你用的是顶层 Worker 做生产,把上面 production 命令里的 --env production 去掉。验收记录里只写四件事:Worker 名称、Wrangler 命令、secret 名称、健康检查返回时间。不要记录 secret 值,也不要截图 Dashboard 里带有敏感字段的页面。

哪些场景要停下来另查?

这篇只处理 Workers secret 在 preview、production、Wrangler environment 之间不一致的问题。遇到下列情况时,排查重点要从 secret 名称切到部署、权限或代码路径:

现象更可能的问题下一步
secret 存在,但 Stripe 返回 401key 值过期或 test/live key 用反去 Stripe Dashboard 看 key 类型和最近轮换时间
production 偶发 500,健康检查一直正常上游 API、数据库、队列或限流看 request id、上游状态码和重试日志
本地 .env 读到了,.dev.vars 不生效本地加载规则冲突二选一使用 .dev.vars.env,别两套一起维护
preview 读错数据库D1、KV、R2、Hyperdrive binding 配错检查 env.preview 下每个 binding 的 id
Dashboard 看到 secret,Wrangler 仍报缺失目标 Worker 或 account 错了核对 account_id、Worker 名称和 --env

还有一个未测范围:Workers for Platforms、Secrets Store beta、Terraform 管理的 Cloudflare 资源,密钥来源会多一层。如果你的团队已经用 IaC 管理 Cloudflare,不要手动在 Dashboard 里临时加 secret,先看仓库里的资源定义,否则下一次 apply 可能把手改内容覆盖掉。