作者用真实 Stripe 账号测试 SaaS 模板,发现 TypeScript 编译通过的代码在 Webhook 中会崩溃。原因是 Stripe 将 current_period_end 从 Subscription 顶层对象移到了 SubscriptionItem,而 retrieve() API 仍返回兼容值但 Webhook 不会。
大多数样板项目都是在 npm run build 变绿的那一刻就交付了。我的也差点这样——直到我决定用真实的测试账户来验证,而不是相信编译器的判断。这个决定暴露了三个真实的 bug,它们本会击中第一个真实用户,而 TypeScript 和一次干净的构建从未对其中任何一个发出过警告。
以下是我的发现,以及为什么"能编译"是一个比听起来弱得多的声明。
处理 customer.subscription.updated 的 webhook handler 直接从事件载荷中读取 subscription.current_period_end:
current_period_end: new Date(subscription.current_period_end * 1000).toISOString()
这个在类型检查时完全没问题,甚至在另一个代码路径中通过 stripe.subscriptions.retrieve() 调用时也正常工作。它崩溃了——RangeError: Invalid time value——只有在原始 webhook 事件载荷上才会。
原因:Stripe 最近把 current_period_end 从顶层 Subscription 对象移到了每个 SubscriptionItem 上,作为其灵活计费 / 多价格订阅变更的一部分。实时代码调用 retrieve() 仍然暴露了一个兼容值,但 webhook 事件中的原始 JSON 没有——在那里它就是 undefined。undefined * 1000 是 NaN,而 new Date(NaN).toISOString() 会抛出错误。
TypeScript 也没有 catch 到它,既没有直接 catch(我安装的 SDK 的类型定义中 Subscription 仍然声明了这个字段),也没有在我通过改用 subscription.items.data[0].current_period_end 修复之后 catch——那时候类型还没有跟上API自己的变更,SubscriptionItem 即使真实 API 返回了这个字段也没有被标上这个类型。我之所以发现这个问题,是因为我用真实的订阅跑了一个真实的 webhook,用 curl 检查了原始载荷,然后看着它返回 500。
Supabase 内置的 auth 邮件发送器默认有速率限制——这是合理的,它本意是用于早期开发,而不是生产流量。我在测试注册流程时多次触发过这个限制。
令我惊讶的是:当达到限制时,supabase.auth.signUp() 不仅仅是没能发送确认邮件——而是整个注册都失败了,根本没有任何用户被创建。粗略看一下 UI 看不到任何错误,账户里什么都没有。如果不是后来通过 admin API 去检查了一下,我就会交付一个注册会静默消失的产品——超过一个低得离谱的、未被文档化的阈值就会消失,直到配置了自定义 SMTP 为止。
着陆页渲染正常——在我的机器上,在我通常测试的主题下。深色模式的系统把 hero 标题渲染成了深海军蓝文字配上近乎黑色的默认背景,因为我从没有在 <body> 上设置过明确的 background-color。当背景不是 white 时,text-gray-900 帮不了你——它和错误的东西一样看不见。
不是编译器。不是 tsc --noEmit。甚至不是一次干净的 next build,在整个 bug #1 存活期间它都通过了。真正 catch 到这三个问题的是:
stripe listen)将真实事件转发到本地运行的服务器这些工具没有一个是稀奇的。这就是"代码通过类型检查"和"产品能正常工作"之间的差别,而对于任何自称生产就绪的东西来说,这个差距正是付费用户发现的 bug 所居住的地方。