页脚写着「零外部请求」,而那一版有 15 页在联网
接上老档案重新生成,样张站上 15 个页面把豆瓣的图片地址印了出来。规矩在同一份代码里写过四遍,一条测试正钉着这个 bug,而告警说了实情却没说后果。
接上 2022–2024 的老档案之后, 样张站重新生成了一遍。上一版是干净的,这一版有 15 个页面带着这个:
src="https://img3.doubanio.com/…"
而那个站点每一页的页脚都写着:
整个过程零网络请求,这个页面也不发一个外部请求。
怎么来的
封面那行代码是这么写的:
douban_cover: coverPath ?? m.coverUrl
↑ 本地没有这张图的时候,退回豆瓣的地址
老档案一张图都没存(前代那个工具只抓 HTML)。 于是那 5 个只存在于老档案里的作品没有本地封面,那行代码就老老实实印上了豆瓣的地址。
这个分支从来没被执行过——扩展抓的档案总是连封面一起抓下来的。 它就那么躺着,等一份没有图的档案。
三件让它更难堪的事
① 规矩在同一份代码里写过四遍
「接不回本地就只留文字,绝不回退到豆瓣的 URL」——
这句话在生成器里写了四遍:广播里的作品链接一遍,被截断的正文一遍,广播卡片标题一遍,
还有一遍就在那行代码上面二十行,
一个叫 realCover() 的函数的注释里,
一字不差地描述了即将发生的这件事。
② 一条测试正钉着这个 bug
测试文件里,一条断言要求「本地没有封面时,豆瓣的地址要被数进 remote 那一栏」——
而紧挨着它下面那一条,解释的正是为什么不该这么做。
两条相邻的断言,结论相反。谁都没发现,因为那个分支从来没跑到过。
③ 生成器告警了,而我把它当成了通知
它是这么说的:
⚠ 有 5 张图没能从档案里取到,页面上留的是 doubanio 的地址
每个字都对。而我读完的反应是「哦,老档案没图,正常」, 然后继续做别的。它其实是在说「离线保证马上就要破了」,而它没有把这句说出来。
一条只陈述事实、不点明后果的告警,会被跳过。 这一条和校验器打完「跳过 schema 层」紧接着打「通过」 是同一个毛病。
修法:没有封面就没有封面
只写 coverPath,取不到就什么都不写。
「没有封面就只剩文字,不放占位图」这条规矩本来就已经在了。
广播的附图是刻意的例外,仍然保留远程地址:那是用户自己传的照片, 留个地址至少说明「这儿本来有张图」。现在有一条测试点名这个例外, 免得哪天它又被悄悄扩大。
顺带清掉的另外两个
一张烂掉的 JPEG,炸掉整趟生成
取图片字节那一步没有任何保护。一个损坏的 JPEG 让整个构建停在这里:
Error: incorrect data check
at payload (src/images.js:92:15)
没有图片名,没有捕获编号,没有档案名,一个页面都没写出来。 而这条链路正好把为什么要核字节演了一遍: 解析器压根不打开图片行,所以它一路绿灯;坏消息隔着两个工具,以一句 zlib 栈回溯的形式出现。
补上保护的同时,「缺」和「读不出来」分成了两栏,因为下一步正好相反:
| 意思 | 该做什么 | |
|---|---|---|
| 缺 | 档案里就没有这张 | 重抓一次,豆瓣还有 |
| 读不出来 | 档案自己的字节坏了 | 重抓没用,得找一份好的档案 |
合成一句「缺 N 张」,等于把人指去做一件不可能成功的事。
还有一条:解压成功之后才写盘——
一个 0 字节的 .jpg 看起来像张图,打开是坏的,那比没有更糟。
一条 2014 年的系统广播,让 Hugo 整站构建失败
广播按月归档,而算月份的函数对着没有时间的广播返回一个字符串 '未知'。
下游没有任何地方认得这个哨兵。于是:
'未知'.split('-') → Number(undefined) → NaN
title: "未知年NaN月"
date: "未知-01" ← Hugo: 这不是一个能解析的日期 → 整站构建中止
2026 年抓的 3411 条广播全都带时间,所以这条路一次都没被走过。 走到它的是老档案里一条 2014 年的系统广播—— 「MewX 开始收听自己的豆瓣FM」,豆瓣自己的模板生成的,确实一个时间都没有。
修法是整个字段不写,而不是编一个日期。Hugo 于是给这一页零值日期, 倒序排下来它落在最后——正是「时间未知」该在的位置。
一个下游没人接的哨兵值,和一个没人读的 verdict 是同一个形状:
它不要钱,直到第一行需要它的数据出现。
共同点
这四个 bug 里,没有一个是新写的代码引入的。 它们全都躺在那儿等一份自己没见过的数据:没有图片的档案、坏掉的字节、没有时间的广播。
而它们被发现,靠的都不是评审——是把老档案接进来,重新生成一遍,然后读产出。