英文跟踪变体与主页面的 canonical 箭头汇合到 /article;独立中文行指向 /zh/article,原请求仍返回 200。

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
客户端中的 Cookie 旁列有 Secure、HttpOnly 与 SameSite。箭头仅把 Cookie 名称和值送往服务端;服务端仍需授权。

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/1.1 实验中,/one、/two、/close 共用一个套接字,/four 使用新套接字。四次响应均为 200,Content-Length: 2,正文 ok 均在下一次请求前完整读完;/close 的 Connection: close 让后续请求重新建连。这是本地顺序实验的实测结果,不是 HTTP/2 或端到端代理证据。

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

HTTP 持久连接允许多次请求与响应复用同一条连接,减少重复的 TCP 建连;使用 HTTPS 时,也可能省去新的 TLS 协商。收益取决于网络延迟、响应大小、服务端处理时间以及池中是否已经有可用连接。连接复用不会消除业务计算,也没有承诺某个套接字永远保持打开。讨论优化之前,先明确观察的是哪一段连接、哪个协议版本。 原文 HTTP/1.1 实验中,/one、/two、/close 共用一个套接字,/four 使用新套接字。四次响应均为 200,Content-Length: 2,正文 ok 均在下一次请求前完整读完;/close 的 Connection: close 让后续请求重新建连。这是本地顺序实验的实测结果,不是 HTTP/2 或端到端代理证据。 原样实验已于 2026-10-17 在 Python 3.14.4 上复验。 完整报文才允许复用 HTTP/1.1 默认允许持久连接,除非某一方发送 Connection: close,或实际关闭传输连接。通常不必额外添加 Connection: keep-alive。客户端连接池可以保留空闲连接,供符合源、代理、证书和池策略的后续请求使用。连续执行两次独立的命令行程序,通常不会共享内存中的连接池,因而不能凭两次响应成功就断言已经复用。 ...

五月 28, 2027 · 1 分钟 · XB Field Notes
收到三秒等待要求后形成最早重试边界,非负抖动把计划放在边界之后;截止期限可以终止策略。

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
原有 Go HTTP/1.1 实验在 IdleTimeout 200 ms 下,让两次 /ping 和执行 350 ms 的 /slow 复用同一连接。响应完成后等待 StateClosed,再发 GET 才使用新编号。另一个不完整请求头探测由 ReadHeaderTimeout 500 ms 约束,读到 EOF 且未调用处理器。客户端 2 s 等待上限另算;未测试 Nginx,本轮也未重跑原文实验。

空闲超时回收边缘连接

空闲超时会在一段时间没有符合定义的活动后关闭连接。它是一项资源保留策略,不是客户端已经消失的证明。健康客户端也可能暂时安静,关键是说清哪种活动重置哪只计时器,以及连接关闭之后,协议允许调用方如何恢复;不能仅凭“连接还在,但没有字节”就判断它应当被立即回收。 原有 Go HTTP/1.1 实验在 IdleTimeout 200 ms 下,让两次 /ping 和执行 350 ms 的 /slow 复用同一连接。响应完成后等待 StateClosed,再发 GET 才使用新编号。另一个不完整请求头探测由 ReadHeaderTimeout 500 ms 约束,读到 EOF 且未调用处理器。客户端 2 s 等待上限另算;未测试 Nginx,本轮也未重跑原文实验。 仅为原文实验示意,本轮未重跑。 空闲属于某个明确的边界 TCP 连接会占用文件描述符、内核缓冲,以及应用或代理记录的连接状态,但通常不需要为每条连接建立独立的路由表项。回收暂时不用的连接,可以限制保留的资源;复用健康连接则能减少重新建连的成本。策略应服务于工作负载,而不是把所有沉默连接都理解成已经被遗弃的泄漏。 ...

四月 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 分帧,不能验证调用方或文件类型,也不能限制所有并发请求的内存总和。它还无法阻止发送方把很小的正文无限拖慢。因此,时间和并发量需要独立预算;不能把字节上限叫作空闲超时,也不能从一次拒绝成功推导出已经具备高负载防护能力。 在暴露副作用前分别限制编码与解码字节。原文 64/8 字节实验的十一次请求仅三次提交;微小上限是测试值,结果属于历史记录。 快速验证 保存为 body_probe.go,使用 Go 1.23 或更新版本运行 go run body_probe.go。环回测试服务允许最多 64 个编码输入字节和八个解码字节,支持无内容编码或单层 gzip。这里刻意使用很小的常量,是为了验证边界,不是推荐生产参数。计数器代表应用提交点,不会真的写入数据库或上传文件。 ...

四月 18, 2027 · 3 分钟 · XB Field Notes
TCP 443 一栏在同一个明文监听上显示 HTTP 收到 200、HTTPS 的 TLS 失败;独立 UDP 443 一栏收到数据报回复。相同数字不合并传输端口空间。

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

默认端口让客户端在协议或配置已经给出数字时省略它。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
相同安全级别下,strict-origin-when-cross-origin 向同源发送来源、路径与查询参数,向跨源只发送来源;两者都不包含片段。

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

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