三个没报错的 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-idx-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 能发的大小
各平台的上传上限(每一条都带来源和核对日期)

有压不动或者结果不对的文件,欢迎丢给我。

posted @ 2026-09-20 18:58  mengyuxuan  阅读(17)  评论(0)    收藏  举报