来源描述的 nginx 1.28.0 实验:AAAA 改成 BBBB,修改时间和四字节长度不变,ETag 相同,旧标签仍得到 304。

nginx 验证器是否可靠,取决于表示身份是否准确

验证器让接收方询问:自己保存的表示,是否仍与当前选中的表示相符。条件 GET 验证成功时,可以返回没有正文的 304,避免重新传输内容。新鲜度是另一个决定,它控制已存响应何时能直接复用而不发送这次请求。ETag 本身不指定缓存寿命,304 也不是一份替代正文;客户端需要保留原响应,才能利用成功验证的结果。 文章描述 nginx 1.28.0 的固定版本反例:修改时间与长度相同,可能让不同字节共享 ETag。本次核验未重新运行该实验。 坏掉的样子 把所有 ETag 都叫作正文哈希,会掩盖 nginx 的一个重要实现细节。在固定版本 1.28.0 的静态文件实现中,生成的标签组合了修改时间和内容长度,并不计算文件内容哈希。如果替换字节时保留这两个输入,就可能保留同一个标签。于是,持有不同旧字节的客户端仍收到看似成功的验证,错误不能仅靠“响应里有 ETag”来排除。 强弱验证器适用于不同比较。对于需要字节身份的用途,强验证器必须能区分表示数据变化;弱验证器则可以把字节不同、但被认为语义等价的表示归在一起。W/ 并不是“接受字节等价变化”的标志,也不意味着这个标签适用于所有前提条件或字节范围续传。使用哪种比较,需要依据具体请求条件,而不是凭标签外观猜测。 条件请求不保证省掉应用计算。服务器可能先生成表示,之后才判断验证器是否匹配。这个静态 nginx 示例根本没有应用渲染器。较小的响应在这里证明节省了正文传输,却不能证明数据库查询、渲染成本或回源请求数必然减少。评估优化效果时,应把实际减少的数据传输和实际减少的后台工作分别测量。 快速验证 下文结果保留自来源中的实验报告,并非本次新运行的 Docker 或 nginx 结果。把脚本保存为 check-validators.py,先取得固定摘要的 nginx 镜像,再用 Python 和 Docker 运行。它通过宿主回环提供一个可丢弃的四字节静态文件,实测 nginx 版本为 1.28.0。实验只控制容器内文件的时间属性,不修改宿主时间,也不修改共享 Docker 虚拟机时钟: ...

二月 17, 2028 · 2 分钟 · XB Field Notes
HTTP/1.1 与 HTTP/2 两条并行轨道都连接 gzip,区分传输选择与响应正文编码。

gzip、HTTP/2 与 Brotli 需要分别验证

HTTP/2 改变 HTTP 传输分帧和多路复用方式;gzip 与 Brotli 编码的是表示数据。启用 HTTP/2 不会自动压缩响应正文,启用 gzip 也不能证明客户端协商出了 HTTP/2。HTTP/2 的头部压缩又是另一种机制。可靠检查应分别记录协商协议、响应 Content-Encoding、传输正文字节数,以及实际解码后得到的内容,不能用其中一项代替其他结果。 坏掉的样子 发送 Accept-Encoding: gzip, br,然后把结果保存成 .br,并不能证明使用了 Brotli。服务器可能选择 gzip,也可能发送仍被请求接受的原始表示。真实编码由响应头说明。根据请求列表或输出文件名猜测,会导致使用错误解码器,也可能让报告声称获得了实际上根本没有发生的压缩结果。文件名只是本地命名,不是协议证据。 Brotli 支持还属于部署能力,并不是安装 nginx 后自然具备的功能。Brotli 过滤和预压缩文件处理由独立模块提供,需要针对实际二进制与配置确认可用性、构建兼容性。添加不受支持的指令可能让配置验证失败,而客户端声明接受 Brotli 不会替服务器安装功能。检查模块时,应以当前程序行为为准,不能从另一台机器的配置推断。 客户端自动解码会制造另一种测量陷阱。curl --compressed 收到较小编码正文后,可以保存成较大的解码文件。把这个文件大小与原始压缩输出比较,实际比较的是不同对象。反过来,只使用 --http2 却不检查协商结果,也可能把允许的协议回落误当成 HTTP/2 已经成功验证。命令选项表达请求意图,结果字段才说明本次发生了什么。 分别核对实际协议、编码与解码一致性。字节数来自历史文章报告,当前集成仍待验证。 快速验证 下文结果归属于历史文章报告,本轮没有重跑;保留的验证文件中未找到原始集成运行日志。运行需备好本地固定镜像并启用 Python 断言;清理仅尽力执行,不能据此保证所有资源已删除。 ...

一月 28, 2028 · 3 分钟 · XB Field Notes
客户端向 /old 发送 POST,收到 308 与 Location,再按原文夹具显式向 /destination 发送第二次 POST。

先决定让 nginx 跳转客户端,还是在内部重写

nginx 跳转与内部重写改变请求流程的不同部分。跳转返回状态和 Location,由客户端决定是否发起新请求;内部重写则在处理现有请求时改变 nginx 当前 URI。两者的选择决定客户端是否看到新地址、location 是否重新搜索,以及最终处理器收到怎样的路径和查询。不能因为都能让某个 URL 得到内容,就把它们当成同一种操作。 坏掉的样子 本来需要内部映射,却改成跳转,会增加客户端往返并暴露新 URL;本来需要公开永久迁移,却只做内部重写,客户端仍会继续使用旧地址。return 和 rewrite 也不能被当成总是默认相同跳转行为的通用缩写。明确状态码与重写标志,才能让预期转换可审阅,避免一行看似简单的配置隐藏行为差异。 状态选择影响方法语义和缓存。307、308 在自动跳转时保留请求方法,301、302 则允许历史上的 POST 改为 GET。永久不意味着每个客户端立刻更新所有已保存链接,临时也不意味着响应绝对不能缓存。缓存指令、请求上下文和客户端行为仍然重要,不能用状态名称替代明确的复用策略。 内部重写还可能选择错误处理器,或反复匹配自己。last 会为改变后的 URI 开始新 location 搜索;break 结束当前重写序列后,继续在现有 location 处理。查询参数可以按替换语法追加或丢弃。把这些区别视为可以互换,可能导致读取错误文件、重复参数,或进入返回 HTTP 500 的循环。 客户端新请求,或内部转换。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 下文集成实验的结果保留自历史运行记录;本次修订没有重新执行该实验,不能将这些记录当成本次环境的新实测。 ...

一月 21, 2028 · 2 分钟 · XB Field Notes
Fetch no-store 取得版本二,原有缓存版本一仍保留;reload 取得并存入版本三。响应 Cache-Control 策略与 Fetch 缓存模式应分开。

Cache-Control 是缓存与源站之间的约定

缓存策略需要行为测试,而不只是检查头部是否存在。两个响应即使都带 max-age,也可能因为其中一个要求验证、另一个禁止存储而表现不同。请求本身同样影响结果:浏览器可以绕过已有副本,却不修改源站的响应策略,也不删除更早保存的缓存条目。要知道用户真正拿到什么,必须观察完整的重复请求过程。 Fetch no-store 取得版本二,原有缓存版本一仍保留;reload 取得并存入版本三。响应 Cache-Control 策略与 Fetch 缓存模式应分开。 仅为来源示意;本任务未执行示例。 双方都要读同一份约定 Cache-Control 指令具有方向性。请求指令不会自动复制到响应里,RFC 9111 还明确将请求指令描述为建议性的要求;响应指令则约束遵守协议的缓存。中间层即使自己不实现缓存,也必须在转发中保留相应缓存指令。部署配置可能另有覆盖规则,但“代理按自己的配置做了”不等于已经证明源站声明的策略得到执行。 应拆开三个问题:这条响应能否存储,当前年龄是否仍在适用的新鲜度寿命内,这次请求是否允许不经验证直接复用。这里的 no-store 禁止 HTTP 缓存存储;不带字段名限定的 no-cache 允许存储,但复用前必须成功验证。示例不包含能够在特定条件下覆盖 no-store 的 must-understand 组合,不能据此推导所有指令组合的行为。max-age 设定新鲜度,must-revalidate 则禁止响应过期后未经验证就复用。响应仍然新鲜时,must-revalidate 本身并不要求每次回源,不能把它误解成 no-cache。 ...

十二月 20, 2027 · 2 分钟 · XB Field Notes
代理始终位于客户端与源站之间,双向转发字节;CONNECT 成功与源站 TLS 验证分别检查。

CONNECT 方法打开一条裸隧道

CONNECT 请求代理建立隧道,并不让代理从数据路径上消失。准入成功之后,代理仍然向两个方向转发字节,并管理套接字、缓冲区和故障处理。CONNECT 本身不提供加密;HTTPS 是在这条隧道内部另行建立并验证 TLS 会话,不能把两个阶段的成功混为一谈。 在正确的边界切换语义 HTTP/1.1 的 CONNECT 目标是 server.example:443 这样的主机与端口组合,而不是普通路径或完整 HTTPS URL。端口必须明确给出。成功的 2xx 响应在响应头结束后立即切换到隧道模式;非成功响应则表示隧道尚未建立,此时不能直接开始把后续内容当作目标服务的数据。 成功的 CONNECT 响应不能携带 Content-Length 或 Transfer-Encoding。随后出现的字节属于隧道,而不是一份用长度界定的 HTTP 响应正文。自己实现中继时,这一点尤其重要:HTTP 解析器已经缓冲的字节也可能属于隧道,不能因为切换到原始套接字,就把此前预读的数据丢掉。 客户端随后可以穿过中继,与目标协商 TLS。代理返回 200 只说明允许并建立了隧道,不负责证明目标证书有效,也不证明应用请求成功。客户端仍须检查信任链、主机名与应用响应。TLS 拦截属于另一种部署方式,需要客户端信任会终止 TLS 的中间实体,不能把它与普通盲转发混为同一种安全模型。 看不见正文,仍然占用资源 盲中继不必解释加密的应用内容,但仍能观察目标地址、字节量、时序和关闭行为。它可以落实连接预算、超时、流量限制,以及撤销准入之后的断开操作。无法读取 TLS 内部的 HTTP 消息,不等于失去所有运营控制,更不等于不再承担带宽和描述符成本。 真正的正向代理应验证调用方身份,并限制目的地。任意主机和端口访问,可能暴露内部服务,或被用于其他协议的滥用。按主机名放行时,还必须考虑地址解析策略:通过名称检查,并不自动证明最终拨号的 IP 地址符合约束。本地实验只允许一个预先确定的环回地址与端口,因此没有引入任意目标的解析和转发能力。 ...

十二月 13, 2027 · 3 分钟 · XB Field Notes
收到的 Age 为 20,共享缓存到 30、私有缓存到 60 即变陈旧。独立陈旧窗口允许响应与异步验证并行,不代表重新新鲜。

缓存的新鲜度要对着时钟判断

响应经过另一个缓存,不代表自动获得一份全新的新鲜度额度。缓存必须计入交付路径上已经积累的年龄,再增加自己的本地停留时间。否则,每一层看起来都只缓存六十秒,串在一起却可能把旧内容直接复用远超源站预期的时间。判断新鲜度需要明确时间来源,不能只把配置中的秒数当成最终交付保证。 收到的 Age 为 20,共享缓存到 30、私有缓存到 60 即变陈旧。独立陈旧窗口允许响应与异步验证并行,不代表重新新鲜。 仅为来源示意;本任务未执行示例。 先算清年龄 Age 估计从响应生成或成功验证以来经过的秒数,其中包括缓存停留与网络传输。收到的 Age 是计算当前年龄的输入,不是在本地存放期间保持不变的计数器。Date 提供响应生成时间;把这些信息联系起来时,时钟同步和实际测得的请求、响应时刻都可能影响结果。只截取一组响应头,不能看到它随后在缓存里又停留了多久。 RFC 9111 给出两条年龄估计路径:接收时刻减去 Date,负数归零,得到表观年龄;收到的 Age 加响应延迟,得到校正年龄值。保守算法取两者较大值,再加本地停留时间。例如请求时刻为一百秒,接收为一百零二秒,Date 为九十五秒,收到的 Age 为十秒,当前时刻为一百零七秒,表观年龄是七秒,校正值是十二秒,当前年龄最终为十七秒。只读原始 Age 就漏算了七秒。 新鲜度寿命也有优先顺序。共享缓存存在 s-maxage 时先用它,否则 max-age 优先于 Expires 减 Date;私有缓存忽略 s-maxage。没有显式过期信息时,在允许的条件下可能使用启发式寿命,但并不存在统一固定时长。只有适用的新鲜度寿命严格大于当前年龄,响应才是新鲜的;恰好相等时已经到达陈旧边界,而不是再多送一次的许可。 ...

十二月 3, 2027 · 2 分钟 · XB Field Notes
容量三的令牌桶从满桶开始。零时刻三次单位成本准入立即耗尽额度,第四次被拒绝。半秒后,每秒两枚的回填速率带来一枚令牌。三种状态展示先突发扣减、后按经过时间回填,不承诺均匀出流。

令牌桶限制准入并允许突发

令牌桶限制获准进入的工作量,同时允许积累的额度在短时间内集中使用。它不会自动把请求均匀隔开,不保证成功完成的吞吐,也不限制仍在执行的操作数量;这些分别属于调度和容量问题。桶的两个参数描述的是预算,不是服务一定能够履行的处理能力承诺。 所选事件账本复现原文注入时间、精确算术的单线程模型。在时长为 Δt 的区间内,理想严格模型的获准成本不超过 B 加 rΔt;均匀出流还需单独调度。历史有限范围执行仍为通过,本图不增加运行结果、分布式原子性或云服务保证。 先补充额度,再扣除工作成本 设桶容量 B 为最多能保存的令牌数,回填速率 r 为每秒新增令牌数。每次请求到来,先将额度更新为 min(B, 上次额度 + r × 经过时间)。只有工作成本不超过当前额度才准入,然后扣除成本。本文采用的严格规则下,被拒绝的工作不消耗令牌。 这种计算可以在请求到达时按需进行,不需要后台定时器不断实际投放令牌。长时间空闲也不能积累无限额度,因为容量会截断回填结果。初始额度是一项策略选择:本例从满桶开始,因此允许立即突发;从空桶开始则会得到不同的初始行为,不能漏写这个条件。 突发额度不是峰值速率 对于这个理想严格模型,在长度为 t 的时间区间内,获准工作的总成本不超过 B + r × t。时间越长,突发项的相对影响越小。这不意味着每个一秒窗口都最多允许 r 个请求,也没有额外给出一个独立的瞬时峰值速率上限。 例如容量为三、每秒回填二时,三个单位成本请求可以在同一时刻全部通过,第四个被拒绝;半秒之后又有一枚令牌可用。如果已获准的工作必须均匀离开入口,还需要实际调度器、有界等待队列或其他节奏控制,不能指望令牌桶自动将同时到达的请求排成匀速流。 准入速率也不等于执行并发量。慢请求尚未结束时,令牌仍会继续回填,允许更多工作进入。需要通过合适的并发限制和超时策略保护工作线程、数据库等稀缺资源。同样,已经准入的请求仍可能在下游失败,所以回填速率不能被写成保证成功完成的吞吐。 预算单位和作用范围要一致 一枚令牌不一定对应一个请求,也可以用加权成本代表字节或预计工作量,但容量与回填速率必须使用相同单位。一个成本超过 B 的请求,无论等待多久都装不进这个严格模型的桶。应明确处理这种情况,而不是给出一个实际上永远不能兑现的重试时间。 还要明确预算按谁区分、由谁持有。四个互相独立的满桶,每个保存三枚令牌,可以同时允许十二个请求;它们不是一个全局三令牌桶。共享限流器必须把回填、比较和扣减协调成一次原子决定,仅共享一个计数器而分开执行这些步骤,并不足以保证配额正确。 本地回填应依据经过时间的时钟,不能让墙上时钟校准凭空增加额度,或使记账时间倒退。Python 的单调时钟适合比较两次读取的差值,但其参考原点不是可以在无关机器之间交换的通用时间戳。分布式时钟和状态归属仍需要自己的明确契约。 坏掉的样子 即使长期速率看起来保守,满桶释放的一次突发仍可能压垮脆弱的下游。无界等待队列会把超额需求转化为内存增长与延迟;直接拒绝和延迟整形是两种不同结果,接口应说明调用者会得到哪一种,不能把所有行为都笼统叫作“平滑”。 应一起观察准入成本、拒绝成本、排队、并发量和下游延迟。重试需要遵守限流策略,而不是立即重新制造被拒绝的负载。按尝试工作计费还是按完成工作计费,也是需要明确的选择;失败后退还令牌会改变模型,并引入额外记账要求,不能悄悄当成同一套公式。 ...

十二月 1, 2027 · 2 分钟 · XB Field Notes
Accept 中 text/html;q=0、*/*;q=1 排除 HTML,而 JSON 的质量值仍为 1。更宽泛的通配范围不会覆盖具体排除。

Accept 系列头让服务器协商应答

内容协商是在服务器能够实际生成的表示之间进行选择。请求偏好只是选择的输入,并不能证明响应已经满足这些偏好,客户端仍需检查收到的表示。Vary 记录影响复用的请求维度,不负责命名最后选中的格式、语言或压缩方式。把选择输入与响应结果混为一谈,会让排查从一开始就看错字段。 分开看待不同维度 请求偏好 所选表示的响应元数据 Accept Content-Type Accept-Language Content-Language Accept-Encoding 实际施加内容编码时的 Content-Encoding 这些维度相互独立,请求 JSON 不等于请求英文或 gzip。Content-Language 描述内容所面向受众的语言,不保证每个字符都属于某一种语言。如果服务器只有英文 HTML 和中文 JSON,不能把分别算出的“最优格式”和“最优语言”拼成根本不存在的中文 HTML。应先列清实际可用的完整组合,再定义选择规则;偏好不能凭空增加服务端的生成能力。 质量值范围是零到一,省略时为一,最多允许三位小数。零表示不可接受,不是“稍微不喜欢”。对媒体类型,一个候选的质量值由匹配它的最具体范围决定。显式的 text/html;q=0 不会被更宽泛的 */*;q=1 解禁。如果只是找整串字段里最大的数字,或者拿逗号前第一项,就会丢掉“哪个范围约束哪个候选”的关系,得到看似按权重处理、实际上违反排除条件的结果。 坏掉的样子 缺失某个偏好字段通常表示在该维度上没有偏好,并不等同于显式拒绝。RFC 9110 允许服务器在没有可接受表示时返回 406,也允许忽略该字段,因此默认响应可能仍然无法被客户端使用。承诺严格协商的 API 必须自己明确这项策略。客户端可以表达排除条件,却不能仅靠发出这个字段,强迫所有现网服务器都执行严格拒绝逻辑。 各类字段还存在不同的匹配语义。Accept 使用媒体范围,语言协商需要约定语言标签匹配方式,Accept-Encoding 则有 identity、字段缺失和空值的专门规则。通用列表解析器不会自动补齐所有业务策略。特别是不能把缺失字段悄悄变成一个空集合,再推导出“客户端拒绝一切”。应用选择默认值与解析器返回没有匹配项,是两个需要明确区分的事实。 相同权重也没有一条跨所有服务器通用的顺序规则。应用应让并列时的选择可预测,并实际测试。完整的语言偏好还可能帮助识别用户,因此记录全部偏好档案、把所有可能字段都放进 Vary,并非天然合理。Vary 只处理匹配问题,缓存资格、新鲜度和授权边界仍需各自的规则,不能因为增加了这个字段就认定内容可以共享。 具体程度、排除与候选顺序决定解析结果;端点仍需单独决定如何把无匹配转换成 HTTP 响应。 快速验证 准备 Python 3.11 或更新版本及 POSIX shell,在临时环境安装本次验证的解析器,再用 "$accept_lab/venv/bin/python" 运行 Python 代码。安装需要外网,实验本身不发请求、不启动服务器。它验证的是 Werkzeug 3.1.8 的候选选择行为,不是假装复现任意浏览器或 CDN 的完整实现。 ...

十一月 23, 2027 · 2 分钟 · XB Field Notes