三个没报错的 bug:多收 3 倍、上线即挂、以及一封永远不会到的邮件
之前那篇结尾我写了句话:浏览器里搞音视频,坑经常是静默失败,连个 error 都不给你留。
后来做云端那个压缩站(vidsmaller.com),又连着吃了三个。
一个比一个不像浏览器的问题,共同点却是同一个:出事的时候,系统报告的是成功。
趁数字还在手上记一下。
一、上游的账单我算错了 3 倍,而我自己的「验证」是假的
上游(FreeConvert)的一个 job 有三个 task:import → compress → export。
我的成本模型写成了:
job_minutes = Σ over ALL tasks: max(1, ceil(task_秒 / 60))
也就是每个 task 各自向上取整、各有 1 分钟地板。于是每个任务都被垫了 3 分钟。
真实规则是只有 compress 计费。 import 和 export 一分钱不花,跑多久都不花。
我后来专门把一次 import 限速拉到 144 秒,账单纹丝不动。
代价不是一个常量写错那么简单:
- 每单最低收费是真实成本的 3 倍,而高估得最狠的恰好是用户最常传的小文件;
- 免费池容量少算 3 倍,月度 30 积分的额度实际只当 10 次用;
- 「R2 中转能省 33% 账单」那条优化,省的其实是 0 分钟(它该留,但理由得换成延迟);
- 我还据此写过一份分析,结论是「竞品每一单都在亏钱」。翻案之后:人家没亏,是正常生意。
真正糟糕的是我当时「验证过」
v2 的验证长这样:模型对三个历史任务算出 12 分钟,去面板上看是 11,差 1 分钟,通过。
这不是对照实验。12 和 11 都没有排他性——很多条完全不同的计费规则都能落在 11 附近。
一个数字跟另一个数字差不多,只能说明它们差不多,不能说明规则是对的。
要证伪一条计费规则,必须设计出让候选规则给出互不相同预测的负载。重做了三组:
| 实验 | 负载 | 候选规则的预测 | 实测 |
|---|---|---|---|
| 地板 | 9 job / 27 task / 11 compress,全部 task < 60s | 每 task 保底 → 27 每 job 保底 → 9 每 compress 保底 → 11 |
11 |
| 长 import | 限速上传把 import 拉到 144s,compress 仍 1.8s | import 不计费 → Δ2 所有秒数累加 → Δ4 |
Δ2 |
| 长 compress | 10 分钟 1080p / medium,墙钟 134.3s | 墙钟 ceil → Δ3 CPU 分钟 → Δ 数倍 |
Δ3 |
第一组里最关键的那个负载是「1 个 job / 7 个 task / 3 个 compress」——
一个 import 喂给三个 compress 再各自 export,把 job 数、task 数、compress 数彻底解耦。
27 / 9 / 11 三个数互不相同,实测只能落在其中一个上。
结论:
job_minutes = Σ over COMPRESS tasks: max(1, ceil(compress_墙钟秒 / 60))
口径是墙钟,不是 CPU 分钟
三组实验一共烧掉 16 分钟配额,大约 $0.14。
顺带两个读数陷阱
上游的官方 API 不暴露任何用量字段(/account/me、/process/jobs、/process/tasks 全都没有)。
真正的读数接口是从网页账号页的 XHR 里扒出来的,好在用自己的 API key 就能调:
POST /v1/process/cpumin/user-subscription-status
body: ["<subscriptionId>"]
→ { remainingMinute: 1400, totalMinute: 1500, updatedAt: "..." }
绝对值、整数、实时。做实验只能信这个。
而网页上的小时图不能用来做实验:那个接口的返回体不含当前这一小时,
刚跑完的任务在里面是 0。直接拿它当读数,你会得出「完全不计费」的结论——
一个更离谱、而且同样不报错的答案。
二、node fetch 不发 preflight,所以测试全绿、浏览器全挂
把压缩站的上传路径从「浏览器直传上游」改成「浏览器先 PUT 到我自己的 R2 桶」那天,
端到端脚本一路绿,推上生产,所有压缩当场全挂。
新建的 R2 桶没有 CORS 策略。 浏览器的预检拿到的是:
OPTIONS → 403 <Code>Unauthorized</Code>
<Message>CORS not configured for this bucket</Message>
用户那边看到的是一句「Staged upload failed. Check your network and retry.」——
一句把责任推给用户网络的话,而实际上没有任何一个用户能靠重试解决它。
我的端到端脚本用的是 node 的 fetch。它不发预检,所以它能顺利通过一条浏览器根本走不通的路。
从此定了条规矩:上传路径的任何改动,必须在真实浏览器里过一遍才算验过。
node 会很高兴地让一个浏览器过不了的测试变绿。
同一段路上还埋了一个:presigned URL 过不了第三方 HTTP 客户端
R2 里的文件要交给上游去拉,我一开始给的是 presigned URL,结果一律 403 SignatureDoesNotMatch。
原因是 AWS SDK 会把 x-id 和 x-amz-checksum-mode 这两个非标准查询参数也签进去,
少任何一个签名就失效;而上游的 HTTP 客户端会归一化 URL,顺手把它们丢掉。
这个也不报有用的错——403 的字面意思是「你签错了」,实际是「中间有人替你改了 URL」。
改法是不用签名:走 R2 自定义域直链,保密性靠 key 不可猜(v4 UUID,122 bit)
加上任务终态后立刻删除,另配一条生命周期规则兜底(compress-input/ 一天后过期)。
实测抓到过 2 个 60MB 的孤儿文件——用户关页面、上传中断、任务创建前就失败,
这些路径主动删除永远够不着。
三、一次硬退信,之后每封邮件都被静默丢弃
域名的 DNS 还在同步的时候,我发了一封测试邮件,吃了个:
550 5.1.1 Address does not exist
正常。等 DNS 好了再发一遍——API 返回成功,邮件没到。
查出来是 Resend 的 suppression list:一次硬退信会把收件人永久加进去,
此后每一次发送都返回 last_event: suppressed。没有错误,没有投递,没有任何一处会告诉你。
curl -s -H "Authorization: Bearer $RESEND_API_KEY" https://api.resend.com/suppressions
curl -s -X DELETE -H "Authorization: Bearer $RESEND_API_KEY" \
https://api.resend.com/suppressions/<id>
这条最阴的地方在于它发生的时机:正好在「我以为已经修好了」之后。
第一次失败有明确报错,我修了;修好之后的所有失败反而是安静的。
如果不是我恰好去查了一次 suppression,我会一直以为魔法链接发不出去是别的原因。
顺带一个同类的:站上印着 support@ 邮箱,而那天之前根域名一条 MX 都没有,
所以四个页面上的每一个「有问题联系我们」都是在往空气里喊。
这个也不报错——用户发过来的信直接消失,我这边什么都不会发生。
想说的一点
三个 bug,一个在计费模型里,一个在浏览器同源策略上,一个在邮件服务商的内部状态里,
互相八竿子打不着。但抓到它们的方式是同一种:
去读那个数的绝对值,然后在真实环境里操作一遍。
- 不是「模型算 12、面板显示 11,差不多」,是设计一个让错误规则必须给出 27 或 9 的负载;
- 不是「端到端脚本通过了」,是自己打开浏览器拖一个文件进去;
- 不是「API 返回 200」,是去另一个收件箱里确认那封信真的躺在那儿。
凡是「看起来在工作」的东西,最好都当成还没验过。代码审查和单元测试抓不到这三个里的任何一个——
它们全都在系统的边界上,而每一个边界的另一侧,都有人正在很客气地对你说「成功了」。
vidsmaller.com 不注册就能用,上面那些实验脚本都还在仓库里,数据可回查。
两个顶着上面这堆账做出来的页:压到 Discord 能发的大小、
各平台的上传上限(每一条都带来源和核对日期)。
有压不动或者结果不对的文件,欢迎丢给我。
浙公网安备 33010602011771号