以观察、判断和行动三个面板呈现规范网址声明的审阅。

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
两行标题旁边的三个相连方块,依次标注 Observe、Decide 与 Act。

Cookie 标记约束支持它们的客户端

Secure、HttpOnly 和 SameSite 分别描述支持这些规则的浏览器如何限制 Cookie。服务端收到的是最终的名称与值,而不是一张证明这些限制已经执行的证书。浏览器行为与服务端授权必须分开:Cookie 完全可能被正确发送,但当前请求的业务动作仍然不被允许,不能把出现 Cookie 头当成全部安全条件已经满足。 分开看设置、发送与脚本读取 Secure 限制 Cookie 通过安全连接发送,通常就是 HTTPS。现代浏览器 Cookie 规则还会拒绝从不安全语境设置 Secure Cookie;localhost 等开发例外应在目标浏览器中核对,而不能推广为任意明文主机都可信。这不仅是发送阶段的标记。一次 HTTPS 重定向无法撤回此前明文请求已经泄露的令牌,因此应结合 HTTPS 与适当的 HSTS 策略,而不是让两者互相替代。 HttpOnly 阻止通过 document.cookie 等非 HTTP 脚本接口读取 Cookie,但不会禁止浏览器把它附到符合条件的脚本请求上。被注入的同源 JavaScript 即使读不到令牌,也仍可能以用户身份发起操作。因此必须单独预防和限制 XSS;HttpOnly 减少的是其中一条窃取路径,并不是把脚本注入漏洞消除了。 SameSite 增加请求语境限制。Strict 通常允许同站请求,Lax 还允许使用安全方法的跨站顶层导航。显式 Lax 不会仅因为某次跨站 POST 是从顶层页面发起,就让 Cookie 自动附上。None 去掉这一项限制,但在支持规则的浏览器中必须搭配 Secure。第三方 Cookie 阻止、存储分区以及 Fetch 的凭据模式,仍可能阻止发送,不能把 None 理解成任何请求都无条件携带。 ...

六月 17, 2027 · 2 分钟 · XB Field Notes
HTTP 持久连接标题卡旁边连接着观察、决策与行动三个面板。

HTTP keep-alive:复用同一条连接

HTTP 持久连接允许多次请求与响应复用同一条连接,减少重复的 TCP 建连;使用 HTTPS 时,也可能省去新的 TLS 协商。收益取决于网络延迟、响应大小、服务端处理时间以及池中是否已经有可用连接。连接复用不会消除业务计算,也没有承诺某个套接字永远保持打开。讨论优化之前,先明确观察的是哪一段连接、哪个协议版本。 完整报文才允许复用 HTTP/1.1 默认允许持久连接,除非某一方发送 Connection: close,或实际关闭传输连接。通常不必额外添加 Connection: keep-alive。客户端连接池可以保留空闲连接,供符合源、代理、证书和池策略的后续请求使用。连续执行两次独立的命令行程序,通常不会共享内存中的连接池,因而不能凭两次响应成功就断言已经复用。 复用的前提是双方都能识别每条消息的结束位置。响应可以通过准确的 Content-Length、分块传输编码,或无正文响应的协议规则界定边界。如果只能依赖关闭连接表示正文结束,这条连接自然无法留给下一次请求。客户端需要读完响应正文;服务器需要读完请求正文,或者关闭连接。残留字节一旦被当成下一条消息解析,问题就从性能退化变成报文错位。 HTTP 持久连接与 TCP keepalive 也是两回事。后者在传输连接空闲足够久后发送探测,帮助识别失联的对端,不负责 HTTP 消息分界,也不能覆盖代理或应用自己的空闲超时。仅仅打开操作系统的探活选项,并不会让一个已经被代理回收的连接重新可用。排查时应分别记录应用层关闭、传输层错误和连接池回收动作。 顺序复用还不同于流水线请求。客户端可以等前一个响应完成后再发送下一条请求,同时一直使用相同连接。HTTP/1.1 流水线允许提前发送请求,但响应仍须按序返回。因此,仅有持久连接并不具备 HTTP/2 多路复用提供的独立响应流,不能把两者的并发收益混为一谈。 每一跳管理自己的超时 空闲超时按照具体实现的规则衡量无活动时间,请求截止时间则限制一次操作能等待多久,两者需要独立配置。反向代理同时拥有客户端连接和上游连接,两侧可能使用不同协议、连接池和超时。因此,浏览器到边缘复用了连接,并不能证明边缘到源站也复用;看到一侧握手减少,也不能直接解释另一侧的延迟变化。 连接池应结合已知的服务端限制提前淘汰空闲连接,但始终可能发生竞态:客户端刚发送下一条请求,服务器恰好开始关闭。监控应同时包含新建连接数、成功复用次数、连接重置、池占用和请求延迟。保留更久可以减少握手,也会占用文件描述符与内存。应依据实际请求间隔和资源预算确定保留策略,不能仅凭“站点很忙”推导一个固定超时。 坏掉的样子 正文长度错误可能让读取方一直等待,或让下一条响应的开头混进上一条正文。遇到请求挂起时先检查消息分界,再考虑调大超时。另一个常见故障是调用方放弃流式响应,却既不读完也不关闭,导致池中的连接长期占用。此时服务器还有处理能力,新的请求却全在客户端等空位,扩大服务端容量无法解决这个问题。 陈旧连接报错不代表服务器没有执行请求。如果支付已经提交而响应在回程丢失,自动重连并原样重发可能重复扣款。是否重试必须依照方法语义和业务幂等契约判断,不能把传输异常当成回滚证明。HTTP/2 虽然可以在连接上复用多个流,却禁止 Connection、Keep-Alive 等 HTTP/1.x 专属字段;把这些头搬进 HTTP/2 配置没有优化意义。 快速验证 下面是可直接运行的 Python 3 实验:服务器绑定系统分配的回环端口,客户端连续请求四次,结束后关闭测试服务器。它不需要外部服务;内置 HTTP 服务器只用于本地验证,不是部署配置。 ...

五月 28, 2027 · 1 分钟 · XB Field Notes
观察、决策和执行面板,旁边是 Retry-After 等待策略标题。

Retry-After 告诉你何时再试

Retry-After 告诉客户端,在发起后续请求之前应当等待多久。它是调度信号,不证明下一次请求一定成功,也不证明重放原操作是安全的。客户端仍然需要截止期限、尝试次数政策,以及哪些操作可以重试的判断,不能把这些责任都交给一个响应头。 结合响应语境解释字段值 RFC 9110 允许非负十进制整数秒数,或者 HTTP 日期。秒数从收到响应之后开始计算,不是从发出原请求时算起;日期则表示一个绝对 UTC 时刻,因此正确解释它依赖客户端墙上时钟足够准确,不能把两个形式都当成“从请求开始算多久”。 发送方必须使用首选的 IMF-fixdate 格式。解析 HTTP 日期的接收方还必须接受两种旧格式,其中旧 RFC 850 格式的两位年份需要遵守五十年解释规则。负数、小数秒和任意文字,都不是合法的 delay-seconds;零和已经过去的日期,则不要求增加服务器指定的等待时间。 配合 503 时,该字段估计服务还会对客户端不可用多久;配合 3xx 响应时,它要求在发送重定向请求之前至少等待相应时间。RFC 6585 另行允许 429 携带 Retry-After,但这里是可选字段,不是每一条限流响应都必须包含的内容。 尊重等待时间,但不无限等待 一个合法等待值可能长于调用方剩余期限。客户端可以停止重试并报告失败,或者在另有授权的流程中安排后续处理;不能只是为了符合本地上限,就缩短服务器要求的等待,然后提前发起请求。很长的值并不自动等于格式错误,也可能是真实的维护安排。 把绝对日期换算成等待时长后,执行调度和经过时间记账应使用单调时钟。真正开始下一次尝试之前,还应重新检查期限,并为这次请求本身保留完成时间。时钟偏差、响应传输和调度延迟,都说明计算出的计划时间与实际请求时间不是同一份证据。 如果字段缺失或无法使用,有界客户端政策可以采用自己的退避规则。但那是客户端的选择,不是服务器给出的恢复估计。必须明确最多尝试几次、总共允许耗费多久,避免一个可选字段让暂时失败变成没有终点的重试循环。 相同等待值仍可能让调用者同时回来 如果成千上万的客户端几乎同时收到相同等待值,它们仍可能在同一个时刻返回。Retry-After 本身不会把这些请求自动摊开到恢复窗口。可以在尊重要求的等待后增加非负抖动,或者用其他不早于该边界的调度方式,但不能向前减掉等待来制造错峰。 组合本地退避和服务器提示时,需要同时保留两者约束。示例取本地退避与解析出的等待值中较大者,再添加正向抖动。注入的偏移量展示的是调度关系,不是实际分布式流量的测量结果,也不是对某个生产环境抖动范围的推荐。 坏掉的样子 当原请求结果未知时,盲目重放写操作可能重复产生副作用。429、503 和 Retry-After 都不会赋予操作幂等保证;重复非幂等工作之前,需要依据应用自己的重试契约,包括操作标识或去重规则,不能只检查响应头是否存在。 ...

五月 4, 2027 · 4 分钟 · XB Field Notes
观察、决策、行动三步示意图,旁边是空闲属于策略而不是故障证明的英文标题。

空闲超时回收边缘连接

空闲超时会在一段时间没有符合定义的活动后关闭连接。它是一项资源保留策略,不是客户端已经消失的证明。健康客户端也可能暂时安静,关键是说清哪种活动重置哪只计时器,以及连接关闭之后,协议允许调用方如何恢复;不能仅凭“连接还在,但没有字节”就判断它应当被立即回收。 空闲属于某个明确的边界 TCP 连接会占用文件描述符、内核缓冲,以及应用或代理记录的连接状态,但通常不需要为每条连接建立独立的路由表项。回收暂时不用的连接,可以限制保留的资源;复用健康连接则能减少重新建连的成本。策略应服务于工作负载,而不是把所有沉默连接都理解成已经被遗弃的泄漏。 Go HTTP Server.IdleTimeout 约束的是持久连接上等待下一次请求的时间。处理器正在工作、但尚未发送任何字节时,并不会仅仅因为超过这个空闲值就被它终止。读取请求头、传输正文、写响应和执行应用逻辑,都需要各自适用的控制项。名字中出现 idle,并不意味着它已经覆盖完整请求的所有阶段。 Nginx 也把不同边界分开:客户端侧的 keepalive_timeout 管理保留的持久连接;proxy_read_timeout 则针对从上游响应读取数据时,两次相邻读取之间的等待,而不是整个响应的总耗时。它们观察代理两侧的不同活动。数值写成一样,并不能让含义也相同,更不能据此断言整条链路已经具有相同的截止点。 零值也不能跨实现直接套用。Nginx 的 keepalive_timeout 设为零,会禁用客户端持久连接;Go 的 IdleTimeout 为零时,则使用 ReadTimeout 的值。如果后者也没有正的限制,才没有建立空闲期限。需要按具体实现核对“零”的契约,不能统一理解成关闭功能,或理解成立即关闭套接字。 坏掉的样子 阈值短于正常静默间隔,会迫使客户端重新建连,或者中断这个计时器实际覆盖的流量;阈值过于宽松,则可能保留超出服务承受范围的套接字。应记录连接数量、关闭原因、复用情况和实际工作模式。安静只能说明被观察的活动没有发生,并不能独立识别坏客户端,也不能告诉你最合适的秒数是多少。 HTTP keep-alive 指的是复用连接,不是定期发送的应用心跳。TCP keepalive 探针又是另一种机制,不会变成服务器读取到的应用请求字节。应用心跳也只有在某个端点确实把它算作活动时,才会影响该端点的计时器。客户端、代理与上游不一定观察到同一批事件,流式传输和多路复用协议尤其需要逐层核对,不能用一个“保活间隔”解释全部行为。 关闭还可能与客户端发起下一次请求发生竞争。连接池中的旧连接需要恢复路径,但重新建连不代表任意请求都可以安全重放。HTTP 明确允许异步关闭连接,是否重试仍取决于请求语义,以及此前副作用是否可能已经发生。“服务器稍微早关一点没有关系”,对修改数据的请求并不是可靠的普遍结论。 快速验证 将代码保存为 idle_probe.go,执行 go run idle_probe.go。程序只使用 Go 标准库,本次在 macOS 的 Go 1.27.0 上验证。测试服务器监听本机回环地址,发送的是普通 HTTP/1.1 请求;没有访问公网边缘节点,也没有启动 Nginx、使用证书或修改任何生产配置。 ...

四月 26, 2027 · 3 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调接收之前必须实际计数输入字节。

请求体限制划定边界

请求体限制约束的是实际接受的输入,而不只是 Content-Length 里写出的数字。应在消费正文时执行限制,并在验证完成之前避免业务提交。压缩输入还需要分别限制编码后的字节和解码后的字节。如果昂贵的内存分配或不可逆写入已经发生,最后再返回 413,并没有守住原本要保护的边界。 明确预算到底计算哪些字节 采用长度分帧时,Content-Length 描述消息正文的长度;HTTP/1.1 分块传输则可能没有预先确定的长度。已声明长度过大,可以提前拒绝,但缺少长度不等于允许无限读取。消息分帧交给 HTTP 实现处理,应用再针对它提供的正文读取器施加自己的预算,不能自行混淆报文边界。 正文读取器不包含 HTTP 头和分块编码的分帧开销,因此这里的限制不是整个网络请求的总字节配额。Content-Encoding: gzip 下,这些正文是压缩后的数据,解码表示可能大得多。应在解压之前约束编码输入,在解压之后再次约束输出;一个很小的压缩包,也可能超过合法的解码大小上限。 限制应来自具体端点的契约,包括单位和支持的内容编码。整个请求体的总量限制,与某个 multipart 文件或字段的限制也不同。流式处理避免一次缓存全部内容,却不会自动限制最终存储量、解析工作或慢速发送方占据请求的时间,不能把“使用流”直接等同于“资源已经受控”。 验证完成之后再暴露副作用 Go 的 MaxBytesReader 在读取超过限制时返回 MaxBytesError。普通 LimitReader 则会在边界处返回 EOF;如果没有额外判断,应用可能把被截断的前缀当成完整输入。最多读取“解码上限加一”个字节,才能区分恰好达到上限的合法正文与确实超过上限的正文。 对 gzip 而言,成功读到一个前缀并不代表校验和有效。在接受未超限的表示之前,需要读到结尾;只关闭解码器不会完成校验。文件发布、数据库修改等动作应放在完整验证成功之后,或者采用带明确中止路径的暂存方案。不能忽略读取错误,仅因为读取器同时返回了一些数据就执行提交。 坏掉的样子 只看 Content-Length 会遗漏未知长度输入;只在解压前使用一个大小上限,会遗漏解压膨胀;先解析、后拒绝,则可能已经花掉需要保护的资源。损坏 gzip 流可以在报错时同时返回正文前缀,错误处理若只检查正文非空,就可能把受损数据变成被接受的业务输入。 字节限制不能修复冲突的 HTTP 分帧,不能验证调用方或文件类型,也不能限制所有并发请求的内存总和。它还无法阻止发送方把很小的正文无限拖慢。因此,时间和并发量需要独立预算;不能把字节上限叫作空闲超时,也不能从一次拒绝成功推导出已经具备高负载防护能力。 快速验证 保存为 body_probe.go,使用 Go 1.23 或更新版本运行 go run body_probe.go。环回测试服务允许最多 64 个编码输入字节和八个解码字节,支持无内容编码或单层 gzip。这里刻意使用很小的常量,是为了验证边界,不是推荐生产参数。计数器代表应用提交点,不会真的写入数据库或上传文件。 ...

四月 18, 2027 · 3 分钟 · XB Field Notes
文章标题旁的三个相连面板,依次标注观察、决策与行动。

默认端口是约定,不负责协议识别或安全认证

默认端口让客户端在协议或配置已经给出数字时省略它。HTTP 常用 TCP 八十,HTTPS 常用 TCP 四百四十三,SSH 常用 TCP 二十二。这些分配减少了配置工作,却不会检查监听发送的字节,也不会建立对端身份。一个端点还包含地址和传输协议;客户端最终使用什么应用协议,必须与监听真正提供的能力一致。 坏掉的样子 把服务迁移到其他端口,不会让现有客户端自动发现变化。URL、数据库连接配置、健康探测、代理和访问规则,可能各自保存目的地。只改服务器,可能让探测仍检查旧监听,或让客户端连接一个已经无人使用的端口。需要记录预期映射,并检查所有受影响的真实调用方,不能仅凭新服务启动成功就宣布迁移完成。 熟悉的数字也容易造成错误信任。进程可以在 TCP 四百四十三提供明文 HTTP,也可以在 TCP 八十提供其他协议。选择 HTTPS 意味着实际执行 TLS 和预期证书检查,单独选择四百四十三不会增加加密。同样,TCP 建连成功既不证明下一步对话符合应用协议,也不证明对端获得了服务该身份的授权。 服务名称数据库描述映射,不管理服务生命周期。Linux 的 /etc/services 将名称映射到端口与传输协议,添加记录不会启动守护进程,也不会打开防火墙。该文件可能不同于另一台主机的数据库或当前 IANA 登记内容。把服务名称转换成端口,与把主机名称解析成地址,是连接建立之前两项不同输入,不能混为同一种发现机制。 快速验证 将脚本保存为 check-default-ports.py,准备 Docker 后执行 python3 check-default-ports.py。固定 Linux 镜像中的 Python 3.11.16 不发布宿主端口。实验有意在 TCP 四百四十三启动明文 HTTP,随后让 UDP 套接字绑定同一个数字。所有监听都可丢弃,不会替换宿主已有的生产 HTTPS 服务,也不会要求修改生产端口分配: ...

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

Referrer-Policy 决定发送多少来源地址信息

HTTP 的 Referer 请求头可以告诉接收方,这次请求由哪个页面发起。它保留了历史拼写,与控制来源信息范围的 Referrer-Policy 并不完全同名。根据策略和目标地址,值可能包含源页面的路径与查询参数,也可能只剩来源部分,或者完全省略。现代浏览器的默认行为并不是向每一个外部目标发送完整地址,不能沿用这种错误前提解释所有来源信息泄露。 当前通常采用的默认策略是严格的跨源仅来源策略:同源请求可以发送路径和查询信息,安全级别合适的跨源请求只发送来源,遇到从 HTTPS 到 HTTP 等安全降级则不发送引用来源。主动设置策略,仍有助于让团队审阅明确的预期行为。只暴露来源虽然少于完整地址,但主机名称本身仍然可能带有业务含义,不能未经判断就把来源级信息一概称为无敏感性的统计数据。 区分几种策略的具体选择 不发送引用来源的策略会省略整个请求头,同源策略只对同源目标保留较完整的值,而不安全地址策略会更广泛地披露来源、路径和查询参数,包括一些严格策略本来会抑制的场景。地址片段会从引用来源计算中移除,因此地址栏里的片段不应出现在接收方的 Referer 中。但这并不表示片段对页面脚本或浏览器状态保密,它只是没有通过这个请求头传送。 文档策略可以通过响应头设置,在合适场景下也可以使用名称为 referrer 的 HTML meta 元素。不能把自造的 http-equiv=“Referrer-Policy” 标签当成同等语法。部分元素以及 fetch 请求还支持单次请求自己的引用来源策略,因此单看文档响应头不一定能解释每一个实际请求。观察到不同结果时,应检查请求由什么上下文发起,以及它是否明确指定了自己的策略。 坏掉的样子 应用如果依赖收到完整 Referer 路径,可能在策略收紧或浏览器隐私行为减少信息后突然失效。引用来源并不是可靠的认证凭据,也不能代替明确的应用参数。同样,修改这项策略不会授予读取跨源响应的 CORS 权限,也不会删除已经写入目标地址的访问令牌。它只控制请求中的一种元数据,不能被当成同时处理全部信息流动的开关。 另一个错误是把跨源与跨站混为一谈。同一主机上的不同端口已经属于不同来源,足以改变本例的策略结果。同源请求也仍然可能把敏感路径或查询信息交给该来源上的另一个接口。需要核对每个接收者实际需要什么;当观察到的值与文档看起来采用的默认值不一致时,还要检查重定向链及资源自己的策略,不能只依据域名相似就认定浏览器表现异常。 快速验证 保存脚本为 referrer-policy.py,执行 python3 referrer-policy.py –serve。在打印出的页面地址后追加 ?policy=strict-origin-when-cross-origin&demo=visible#fragment-not-sent 并打开。本次使用 Python 3.14.4 与 Chrome 152,两个临时回环服务分别提供页面和跨源接收端。页面实际请求同源接口与跨源接口,由接口返回自己收到的 Referer 值,结束后按控制键加 C 关闭两个服务。 ...

三月 24, 2027 · 2 分钟 · XB Field Notes