观察、决策、行动三个方框,旁边强调返回真实状态并引导读者。

404 页面诚实地作答

404 响应有两类读者:软件需要准确的状态码,访客需要一条能继续前进的路径。错误页即使设计精美,也不能用正文弥补错误的 200;状态码正确,也不代表页面可以难以阅读或没有导航。应当同时保证协议和内容诚实,而不是把一个缺失地址误写成成功,或者把正常的缺失当成整个业务故障。 在渲染正文前确定状态 RFC 9110 对 404 的定义是:源站没有找到目标资源的当前表示,或者不愿透露它确实存在。这并不说明缺失是暂时还是永久。如果服务端知道资源很可能已永久删除,可以使用 410;如果地址确实迁移,就把它重定向到对应的新资源。不要把所有未知地址都送往首页,因为这样既掩盖坏链,也会把访客带到与原意无关的地方。 依赖故障和资源缺失不是一回事。数据库连接失败,导致应用无法判断记录是否存在时,应保留对应的服务端错误语义,不能把所有异常统一变成 404。另一方面,权限策略有时会故意用 404 隐藏资源存在性;这种策略要在契约中说明,而不能在错误正文里泄露对象编号、账号归属或权限细节。相同状态码的选择,必须能由业务事实和安全策略解释。 Google 可能把带错误内容的成功响应识别为 soft 404。返回 200 不保证页面被收录,返回 404 也不保证搜索结果立刻消失。工程上可以承诺的是准确状态、明确内容和正确链接,不能把搜索引擎的后续抓取与索引处理说成即时、确定的操作。修复坏链以后,还需要观察实际抓取和访问结果,而不是仅凭模板名称宣布问题消失。 让错误正文独立且安全 页面用一句简短说明、一个真实首页或目录链接帮助读者恢复方向即可。静态站没有必要为了错误页新增搜索表单、第三方组件或数据库查询;本地静态资源和简单模板更容易在故障时继续工作。API 则应返回已经约定的机器可读错误格式,并保留 404,不能返回一份状态为成功的 HTML 让调用方误判。 状态必须先于正文写入。在 Go 中,如果还没有调用 WriteHeader,第一次写正文会隐式选择 200,后面再试图设置 404 就已经太晚。HEAD 应保持与 GET 相同的状态和适用的表示元数据,但不能传输正文。直接打开某个错误模板文件,只证明那个文件能显示,并不证明真实的缺失请求会进入正确处理器。 坏掉的样子 把所有路径重写到首页,会让缺失文章、脚本和样式表都得到假成功;反向代理也可能把源站的 404 改成自己的 200 错误页。另一个常见问题是处理器先输出页面框架,随后才发现文件不存在。排查时应在实际入口请求一个确定不存在的地址,先禁止自动跟随重定向,再对照正常地址的第一份响应,而不是只看浏览器最终显示了什么。 ...

七月 3, 2027 · 3 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现 sitemap 条目验证。

Sitemap 帮助爬虫发现页面

Sitemap 为爬虫提供一份明确的候选网址清单,适合帮助发现新内容或普通导航中较深的文档。这份列表不会绕过登录、授予抓取权限、保证搜索收录,也不能承诺某天一定重新访问。应把它视为计划公开文档的发布清单,并让清单与规范网址及实际发布状态保持一致。文件成功上传,只能说明某一步交付发生了,不能替代后续抓取和收录证据。 从发布清单生成 使用与公开页面相同的权威内容筛选规则,排除草稿、私有文档和多余的跟踪参数变体。源码目录里存在一个文件,不代表它已经对应公开 URL;反过来,分页、翻译文档或生成路由,也未必与源文件逐一对应。因此需要查看生成器实际输出,而不是仅凭目录列表猜测发布范围。内容筛选与 sitemap 生成相互独立维护,容易出现遗漏或提前暴露未发布入口。 XML sitemap 使用声明的命名空间,以及包含网址条目的 urlset,每个 loc 提供完整网址。XML 转义与 URL 编码解决不同问题:分隔查询参数的与号,需要在 XML 文本里转义,而网址本身的字符也必须符合相应 URI 表示方式。生成后应重新解析文件,确认经过两层处理,仍然得到原本希望发布的网址,不能因为 XML 看起来能打开就省略这一检查。 还要遵守格式的范围与大小条件。通常应列出相应主机及允许位置范围内的网址,受支持的跨站提交安排有额外规则。每份 sitemap 最多五万个网址,未压缩内容不得超过 52,428,800 字节。gzip 可以减少传输量,却不能取消解压后的上限。更大的清单应拆分并使用适当索引,不能为了让文件通过检查而悄悄截断后面的页面。 这份清单本身也是公开数据。私有文档没有出现在导航里,并不意味着它的网址可以放心写入公开 sitemap。发布前应检查生成 XML 是否暴露了不该公开的入口,同时在资源处独立执行访问控制。爬虫发现偏好与权限控制是不同机制,清单筛选不能替代真正的授权检查。 坏掉的样子 XML 合法的文件,仍可能列出跳转、缺失页面或不应进入索引的内容。应同时验证目标与语法,并让目标上的 canonical 声明符合预期清单。某个地址返回二百,却只有登录界面或通用错误页,不能证明列出的文档真正可用。Sitemap 自身成功交付,与其描述的页面是否有用,需要分别检查,不能在一个状态码上完成所有判断。 修改时间也可能失真。提供 lastmod 时,应依据页面有意义的变化,而不是每次构建都刷新所有条目的时间。Google 明确说明,它会使用持续准确的 lastmod,并忽略 priority 和 changefreq。这些字段不是调度接口,把所有页面都标成最高优先级,也不能命令爬虫增加访问。没有可信历史依据时,省略可选字段比编造精确时间更诚实。 ...

六月 30, 2027 · 2 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现规范网址声明的审阅。

Canonical URL 标识首选表示

同一文档可能由多个地址提供:跟踪参数、别名主机或末尾斜杠变化,都可能不改变有意义的内容。canonical 链接用于指出合适重复表示中的首选版本,帮助接收方理解它们的关系,但不会自动重定向 HTTP 请求,也不能强制搜索引擎采用声明的网址。只有目标与周围信号符合真实内容关系,这份声明才有价值;标签存在本身并不等于规范化已经完成。 先确定身份,再写标签 先决定哪个公开网址代表这份文档,再让站内链接和相关 sitemap 条目与这个选择一致。不能机械删除所有查询参数:跟踪参数可能多余,语言、页码或商品选择却可能实质改变表示。如果没有比较含义就把它们全部折叠到一个地址,会把不同文档指向错误的首选页面。规范化应当反映资源关系,而不是只让网址看起来更短。 HTML 中应把带有 rel="canonical" 的 link 元素放入有效 head,并提供清晰目标。对于 PDF 等其他表示,也可以用 HTTP Link 响应头表达这一关系。优先采用完整公开网址,减少 base 地址或部署前缀造成的歧义。生成时应依据站点预期的公开配置,而不是意外把代理内部主机名写进页面。实际发布结果需要再次检查,不能仅查看模板参数。 canonical 链接关系本身不要求目标必须位于同一个源站。跨域声明需要仔细审查内容与控制关系,声明一个值并不能证明拥有目标。对翻译页面,如果同语言的合适规范页面存在,通常应选择该语言目标。语言替代与内容重复是不同关系,把所有中文页面都指向英文首页,会抹掉有意义的文档区别,不能称为准确的去重处理。 坏掉的样子 声明目标可能报错、继续重定向,或者又指向另一个 canonical。模板、插件和响应头各自生成声明,也可能产生相互冲突的值。应检查完整响应,而不是只搜索有没有一个标签。首选目标需要能够使用,声明也要与实际表示一致。多个位置各自维护相同事实时,更应明确权威来源,避免一次修改只更新了其中一处。 普通链接关系不会改变原始地址的可访问性,除非路由另外配置了行为。如果旧路径需要对所有客户端迁移,就应设置合适的 HTTP 重定向,并验证它对方法的影响;如果变体仍有用途,也可以继续成功响应并提供 canonical。不能把元数据提示等同于跳转、访问授权,或者爬虫必须执行的命令。用户与搜索系统可能观察到不同层面的结果。 搜索引擎会综合多种信号,也可能选择另一份规范网址。因此,标签正确不等于收录已经收敛。应区分源站实际提供的声明与搜索服务报告的选择,排查站内链接、sitemap、跳转或语言元数据是否互相矛盾。发生分歧时,不应只是在更多位置重复添加同一个标签,而应先确认它表达的内容关系是否真实且一致。 快速验证 保存为 canonical.py 并执行 python3 canonical.py。脚本创建临时回环 HTTP 源站,用 Python HTML 解析器读取实际响应,提供英文文档、跟踪参数变体、中文文档和两个故意不一致的元数据案例。本地网址只是演示身份,并不会提交给搜索引擎,也没有检查任何真实搜索账号或收录记录。 ...

六月 26, 2027 · 2 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现 well-known 端点验证。

Well-known 路径提供的是发现约定

Well-known URI 为软件发现某个源站的信息提供约定位置。保留的 /.well-known/ 前缀,减少了不同协议各自设计路径时发生名称冲突的机会;注册后缀则指向定义它的具体规范。这个前缀是一种命名机制,不是自动实现,更不是内容可信的保证。服务器仍需正确路由所支持的端点,客户端也必须按照该端点自己的契约解释响应。 按具体后缀的规范实现 RFC 8615 为支持的 URI 方案保留这一命名空间,不要求每个网站发布所有已注册资源,也没有给所有后缀定义统一响应格式。确定媒体类型、认证、跳转处理、缓存或传输条件之前,应查看 IANA 注册及其引用规范。路径看起来熟悉,不能替代协议规则;不同自动化客户端也不会因为共用前缀,就自然采用相同的读取和验证方式。 对于通过网站发布漏洞联系信息,RFC 9116 规定 security.txt 位于 /.well-known/security.txt,并要求通过 HTTPS 获取。旧位置 /security.txt 可以重定向到标准位置。文档使用 UTF-8 纯文本和规定字段,包括至少一个 Contact 以及唯一的 Expires 字段。Contact 提供报告渠道,不代表邮箱一定有人值守,更不能证明某份报告已经被接收或处理。 文档中的 Expires 字段用时间戳限制信息的新鲜度,它与 HTTP 响应中的 Expires 头不是同一个东西。传输层返回二百,不会让已经过期的联系信息重新变得有效。地址和政策需要持续维护,并在内容过时之前更新。使用 Canonical、Policy 等可选字段时,也应遵守各自规则,不能把文档里任意一个网址都当成已经验证的可信声明。 坏掉的样子 宽泛的应用回退路由可能对任何未知地址都返回首页或登录页,并统一使用二百状态。浅层健康检查认为路径可达,真正的协议客户端却得不到可用文档。因此要保留准确路由,同时检查媒体类型与内容。意外跳转到另一个源站,还可能改变信任上下文,而不只是移动一个文件。仅检查最终状态成功,可能遗漏这些协议层面的错误。 命名空间本身不会认证内容。HTTPS 校验帮助建立到目标主机的连接,文档验证及协议要求的其他检查,才进一步判断内容是否可用。存在安全联系信息,不等于授权外部人员扫描或攻击服务。部署也不能因为一个标准资源计划公开,就不加区分地暴露整个目录。应围绕实际支持的资源配置路由和权限,而不是把前缀当成自动开放所有内容的规则。 ...

六月 22, 2027 · 2 分钟 · XB Field Notes
观察、判断和行动三个面板,旁边是文本字节解码主题标题。

字符集告诉阅读者这段字节该如何解码成字符

字符编码规定字节与字符之间的对应关系。在 text/plain; charset=utf-8 中,字符集参数声明了这层关系,但修改参数不会改变正文。可靠的文本交付要求生产者实际使用的编码与消费者采用的解码规则相符,不能只检查配置文件里是否出现了 UTF-8 这个名称。标签、字节和格式规则需要共同成立。 先由格式确定规则 应用通常先把内存中的字符序列编码成字节,再发送响应;接收方则在处理文本之前把字节解码回来。如果响应还经过压缩,需要先解除 gzip 这样的内容编码,再进行字符解码。这是两次不同的转换,不能因为名称里都有“编码”就当成同一层。数据库连接、模板引擎和响应生成器都可能引入不一致,因此最后一个 HTTP 响应头写对了,并不能证明之前的转换过程正确。 HTML 遇到声明冲突时并不是任意猜测,而是有明确的编码探测算法。对于没有用户手动覆盖的普通网络解析,能够识别的字节顺序标记先于受支持的传输层编码声明被考虑,正文里的声明则在后面。因此,页面中的 meta 一般不能覆盖明确且受支持的 HTTP 字符集。应让两处都准确,并把 HTML 编码声明完整放在前一千零二十四个字节之内,而不是依赖冲突优先级去挽救错误输出。 各种文本格式没有一个统一的“缺失字符集时默认如何解码”规则。RFC 8259 要求非封闭生态系统之间交换的 JSON 使用 UTF-8,它的媒体类型注册没有定义 charset 参数,而且网络发送方不得在 JSON 前添加字节顺序标记。不能因为 JSON 和 HTML 都能看成文字,就把 HTML 的探测规则搬到 JSON 上。应先确定内容格式,再执行这个格式规定的编码契约。 坏掉的样子 仅包含 ASCII 的测试很容易掩盖错误,因为这些字符在 UTF-8 和常见旧编码中的字节相同。测试样本应包含重音字符、货币符号,或产品真实支持语言中的代表性文字。一份 Windows-1252 正文如果误标 UTF-8,严格解码可能直接报错;反过来,本实验的 UTF-8 字节被当成 Windows-1252 时却能成功解码,只是字符已经变了。因此,“没有异常”不是文字正确的证据,必须与已知原文比较。 ...

三月 5, 2027 · 2 分钟 · XB Field Notes
三个相连的方块,依次标注观察、决策与行动。

CORS 预检取决于请求形态

CORS 预检用于询问浏览器能否发送某种具体形态的跨源请求。它是一条 OPTIONS 请求,携带计划采用的方法及相关请求头名称,浏览器检查服务端答复后才决定是否发送实际请求。这与实际响应到达之后,检查应用脚本能否读取响应,是两个不同阶段。它们都可能失败,但对服务端是否执行过业务操作的影响不同,诊断时必须先确定错误发生在哪个边界。 触发预检的条件并不是简单的“带有凭据”或“会产生副作用”。在其他条件同时满足时,安全列表中的 GET、HEAD 或 POST 可以不预检。PUT、应用自己设置的 Authorization 等请求头通常需要预检,JSON 内容类型也不属于安全列表里的内容类型。因此,即使目标地址和业务操作没变,只改变请求形态,也可能新增一次 OPTIONS 交互。不能把这种变化误认为服务端突然多执行了一次业务请求。 把每项权限放到对应阶段理解 预检携带计划使用的方法,并在需要时携带计划设置的请求头名称。响应中的允许方法和允许请求头必须覆盖浏览器提出的内容。这个阶段成功之后,实际响应仍然要满足自己的 CORS 暴露条件。当获准的前端需要读取错误状态和正文时,错误响应也可能需要恰当的暴露头。后端处理函数可以被普通客户端成功调用,并不足以证明浏览器中的真实前端能够访问它。 凭据模式又增加了独立约束。使用 include 时,响应暴露需要匹配的明确来源,以及值为 true 的允许凭据头;即使本次测试根本没有 Cookie 可发送,允许来源通配符仍然不成立。浏览器对 Cookie 的其他限制也独立存在。允许凭据头不能替代 Cookie 配置、用户认证或来源允许列表,也不应为了回答浏览器的预检元数据查询,就要求预检先携带会话 Cookie。 坏掉的样子 应用路由可能允许 PUT,但预检响应遗漏了 PUT 或所需自定义请求头。浏览器此时只发送 OPTIONS,根本不会派发实际 PUT。重试业务处理器或修改数据库代码,并不能修复这个协议边界。反过来,一个采用凭据模式的简单 GET 可以在没有 OPTIONS 的情况下到达处理器,然后因为服务端返回通配符而无法向脚本暴露响应。仅凭前端的失败提示,无法确定哪一种情况发生了。 预检缓存还会让配置变更难以观察。最大缓存时长控制的是浏览器专门维护的许可缓存,并且受到浏览器自身限制;它不是业务接口正文缓存。判断修改是否生效之前,应查看真实请求交互及缓存影响。如果响应会根据请求来源选择不同的允许值,还要设置恰当的缓存变化规则,避免共享缓存把某个来源对应的许可响应直接复用于其他请求。不能靠反复修改接口正文来排查权限缓存。 ...

一月 22, 2027 · 2 分钟 · XB Field Notes
观察、决策、行动三个面板,旁边的标题说明Caddy 自动处理 HTTPS 证书签发与续期。

Caddy 自动管理 HTTPS 证书的签发与续期

Caddy 可以自动获取并续签受管理的证书,不需要运维人员手工实现 ACME 协议。自动化省去了重复操作,却没有取消签发前提,也不能代替对失败的监控。今天证书有效,不证明下次续期仍能完成,更不证明整条链路中的每一跳都使用同一张证书。应先明确服务名称、验证路径、持久存储和 TLS 终止位置,再判断自动化覆盖到哪里。 先让自动 HTTPS 知道服务名称 当配置让 Caddy 知道符合条件的主机名或地址时,自动 HTTPS 才会按规则启用,同时还要考虑文档列出的例外与显式覆盖。Caddyfile 中的站点地址包含主机名,是常见触发方式。file_server 指令负责提供文件,本身并不等于为某个尚未指定的域名申请公开证书,不能把文件服务功能与证书管理的启用条件混在一起。 命令行区别很明确:不带参数的 caddy file-server 默认监听八十端口;提供 --domain 后,才会尝试为指定主机名启用 HTTPS,并将默认监听端口改为四百四十三。无论命令怎样拼写,都不能单独证明 DNS、端口转发或证书授权已经正确。显式 HTTP 配置、手动加载的证书等条件,也可能改变自动行为,应该检查实际生效配置。 本地开发还有不同的信任边界。对于内部名称,Caddy 可以使用自己的本地证书机构。客户端需要通过适当流程信任该机构;某台机器能够正常访问,不代表这张证书已经获得公网客户端的普遍信任。更不能为了修复客户端信任问题,把私有 CA 的签名密钥发给所有客户端。信任根分发与私钥保管是两项不同工作。 让挑战方式与真实路径匹配 Caddy 默认同时启用 HTTP-01 与 TLS-ALPN-01。前者通过公网八十端口验证,后者通过四百四十三端口上的特殊 TLS 握手验证;Caddy 可以在可用方式之间选择。某次 HTTP 挑战失败,并不说明 DNS 验证就是唯一替代方案。应先从日志确认实际失败的是哪种挑战,再检查该方式的入口、转发和防火墙规则。 ...

一月 21, 2027 · 2 分钟 · XB Field Notes
文章标题旁排列着由箭头相连的 OBSERVE、DECIDE、ACT 方框,下方有圆形指针和引导线。

Link 头把相邻的资源关联起来

Link 响应头描述当前资源与其他资源之间的关系。一个返回 JSON 的接口可以用它给出下一页,让客户端不必解析 HTML,也不必猜测分页参数。这个字段提供目标地址和关系类型,是否访问仍由客户端决定。它是可供理解的元数据,不是强制浏览器下载资源、要求爬虫收录页面或授予访问权限的指令。 链接包含上下文与目标 例如 </items?cursor=b>; rel="next",表示从当前上下文资源指向指定目标。没有显式的 anchor 参数时,上下文通常是被请求的资源。相对目标应根据重定向之后的有效请求地址解析,不能根据程序工作目录或想象中的接口根路径拼接。只包含查询串的目标会替换查询参数而保留路径,这与以斜杠开头、从站点根部解析的地址不同。 rel 指出关系类型,注册关系包括 next、prev、alternate 和 canonical,扩展关系还可以使用 URI 标识。type="application/json" 等参数描述的是目标资源,不会改写当前响应实际的内容类型,也不保证目标可信。如果一个链接通过 anchor 改变了上下文,无法处理这种上下文的客户端就不应擅自把它解释成当前页面的关系,否则链接含义已经改变。 不同关系还对应不同消费者。爬虫可能参考 canonical 选择优先收录地址,支持相应机制的浏览器可能根据 preload 提示提前获取资源,但这两种关系都不会把当前请求变成重定向。需要将字段语法验证与目标消费者的行为验证分开:普通 HTTP 库完全可以正确返回这个头,却不主动跟随或下载任何目标。 分页沿服务端契约前进 分页接口由服务器判断是否存在后续结果,并给出下一次应当请求的完整目标。游标可能编码排序、筛选条件和快照位置,应当作为不透明值使用。客户端自己递增页码、解码后改写游标或丢掉返回链接上的查询条件,都可能跳过数据、重复读取或使请求失效。跟随服务器提供的目标,才能让客户端适应接口内部分页方式的调整。 在具体接口的分页契约下,没有 next 通常意味着本次遍历结束,但不能据此推断整个集合从此不再变化。若没有快照语义,并发插入与删除依然可能改变两次请求之间的成员。采集程序应设置请求上限,并识别反复出现的续页目标,以免服务端错误导致无限循环。遇到限流或失败也不能直接当成最后一页,否则得到的是被误标为完整的残缺集合。 坏掉的样子 一个响应可以用多条 Link 字段,也可以在同一字段中用逗号分隔多个链接。直接按每个逗号切分会出错,因为带引号的参数值、甚至 URI 引用本身都可能包含逗号。应使用符合 Web Linking 语法的解析器,保留重复字段并正确处理关系标记。如果代理把多条字段覆盖成最后一条,正文虽然仍然有效,下一页信息却可能已经丢失。此时应比较源站和代理后的原始头部。 ...

十二月 4, 2026 · 1 分钟 · XB Field Notes