HTTP/3 与 QUIC 标题卡旁边连接着观察、决策与行动三个面板。

HTTP/3 分离流的交付顺序,不隔离全部恢复机制

HTTP/3 把 HTTP 放在 QUIC 上传输,不再依赖 TCP。QUIC 提供逐流有序交付:一条流缺少某段内容时,另一条流已经完整收到的数据不一定必须跟着等待。它解除的是特定的传输顺序依赖,并没有给每个请求配备独立的拥塞控制器、丢包检测器或确定的完成期限。先区分交付顺序和恢复机制,才能准确理解收益。 交付顺序不等于丢包检测 HTTP/2 的帧共享 TCP 的一个有序字节流。中间缺少一段时,后续字节即使属于另一个响应,也要等缺口补齐才能交给 HTTP。已经交付的内容不会被收回,应用也不会因为出现一次丢包就全部停止。究竟影响哪些请求,要看它们的字节相对缺口处于什么位置,不能笼统说整条连接上的所有工作都同时冻结。 QUIC 在每条流内维护偏移量。当 A 流仍缺少前面的字节时,B 流完整收到的数据可以交付。不过,一个 QUIC 包可以携带多条流的帧,因此丢掉一个包也可能同时影响多条流。需要恢复的信息会装进新包再次发送;包编号与流偏移量承担不同职责,并不是每个流各自重新传送一份独立的旧包。 RFC 9002 按包编号空间区分丢包检测,而不是按 HTTP 请求或流划分。往返时间估计与拥塞控制面向网络路径。逐流流控之外还存在连接级额度,因此共享容量不足或连接额度耗尽,仍会同时拖慢多个流。不能把独立排序扩写成独立带宽,也不能说某条流发生问题时邻居在任何条件下都不受影响。 HTTP 语义与部署边界仍然存在 方法、状态码和表示语义仍属于 HTTP。HTTP/3 改变消息分帧,并使用 QPACK 压缩字段。QUIC 集成 TLS 1.3 认证与包保护;调度和优先级策略仍要决定先发送哪些已经可用的流数据,并不会凭空增加带宽。如果某个字段区段依赖尚未到达的 QPACK 动态表项,它仍然可能等待。因此,去掉 TCP 的跨流顺序约束,并没有消灭协议内部或应用内部的全部依赖,共享数据库锁和服务端工作队列也不会随之消失。 丢包路径可能受益,但应测量真实工作负载,而不是保证所有网页必然加速。处理器成本、握手成功率、调度、可用带宽和客户端实现都会影响结果。比较时要核对同一被测链路实际协商的协议及正文完成情况;边缘节点使用 HTTP/3,不能证明它连接源站时也采用同样协议,更不能凭协议名称推算生产收益。 ...

三月 21, 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
以观察、判断和行动三个面板呈现 HTTP Basic 认证机制。

Basic Auth 携带的是可重复使用的凭据

HTTP Basic 认证把编码后的用户名与密码放进 Authorization 请求头。由于编码可逆,取得该请求头的值,往往就相当于取得凭据本身。Basic 确实定义了认证挑战与响应流程,但不会用密码学方式把响应绑定到服务器刚生成的一次性随机数。只要服务器继续接受这个账号和凭据,一份曾被接受的值就可能被再次使用。不能把“存在挑战流程”误解成“重放一定失败”。 看清真正发生的交换 服务器可以返回四零一,并通过 WWW-Authenticate: Basic 挑战声明 realm,客户端再带 Authorization 重试。客户端也可以在合适的保护空间内提前发送凭据。因此,带认证的请求会携带凭据,不意味着每个最初请求一开始就必须有它。realm 用于标识保护空间,并不会自动授予同名路径上的所有权限,更不能替代具体的资源访问控制。 编码输入由用户标识、一个冒号和密码拼接。第一个冒号是分隔符,所以用户名不能包含冒号,密码则可以包含后续冒号。用户名和密码都不能包含控制字符;允许密码里出现冒号,并不意味着它可以包含任意字符。Base64 既不是加密,也不是密码哈希。涉及非 ASCII 凭据时还要考虑字符编码:RFC 7617 在兼容 ASCII 的条件下没有统一默认编码,并允许服务器挑战指明 UTF-8。仅验证一个 ASCII 示例,不等于已经覆盖所有国际化账号的编码与比较路径。 Basic 本身不定义会话令牌或通用退出登录协议。应用仍可以设置账号到期、禁用账号、改变授权或拒绝旧密码。因此,轮换密码并不是阻止泄露请求头继续生效的唯一办法。凭据的有效期与撤销方式,应依据服务器真实的账号策略定义,不能期待网络传输格式自动提供完整账号生命周期管理。客户端停止发送,与服务器已经拒绝旧值,也是不同事实。 坏掉的样子 在没有保护的网络链路上,能够读取请求头的人可以解码并再次使用。正确校验证书的 HTTPS 保护的是到 TLS 终点的传输,不会让终点本身、合法终止 TLS 的代理或疏忽的应用日志看不到凭据。还需要保护后续链路,并避免在日常追踪、报告和共享诊断文件中记录 Authorization 的真实值。传输加密不能替代服务端对凭据的妥善处理。 权限过大的凭据会扩大泄露后果。账号应匹配具体任务及其生命周期,身份验证之后仍要执行授权。凭据有效,不等于每个操作都允许;realm 标签也不是租户隔离机制。客户端需要限制凭据发送位置,尤其关注重定向和脚本复用全局请求头的情况。把同一个认证头传给无关目的地址,超出了原来的保护空间,也破坏了凭据使用边界。 重复的四零一可能来自凭据缺失、错误、到期或服务端不支持相应方式。在下面例子中,四零三表示已经识别的演示账号缺少目标权限,但不能据此认为所有服务返回四零三都证明认证已成功。应结合服务契约及响应中的认证挑战检查,不能仅凭一个数字推断完整的身份状态。认证结果和业务许可,应分别提供能够判断的证据。 快速验证 保存为 basic-auth.py,执行 python3 basic-auth.py。所有凭据都是刻意编造的演示值:用户名为 DEMO,密码为 NOT:A:SECRET。服务器只绑定回环地址,用明文 HTTP 展示协议机制;不要把这些值替换成真实凭据。实验没有测试 TLS、持久账号存储或生产认证服务,也不会读取当前环境的登录信息。 ...

二月 11, 2027 · 2 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现有界陈旧缓存决策。

陈旧缓存需要有界策略

陈旧响应可以在源站故障时维持公开手册的可读性,也可能保留过时价格、已撤销权限或误导性的状态。可用性并不天然高于正确性。启用这种行为之前,应先确定哪些信息允许继续使用、什么条件下可以复用,以及最多能旧多久,而不是看到一次错误响应就无条件拿旧成功结果代替。 用可用性换新鲜度 过期和淘汰是两件事。过期响应仍可留在缓存里,用于条件验证或受到许可的陈旧响应;但冷缓存或已经淘汰条目的缓存,没有旧正文可返回。陈旧指令无法凭空创造副本,保存着字节也不会自动授予复用权限。选中的响应还必须匹配当前请求,包括相关 Vary 维度和访问边界,不能拿另一个请求留下的成功内容填补故障。 如何表示 stale-while-revalidate 允许在有界窗口内返回旧响应,同时尝试不阻塞该响应的验证。stale-if-error 则允许在尝试访问源站遭遇符合条件的错误时使用旧响应。两者触发条件不同,都不强制缓存保存内容,也不能保证每种缓存实现都支持。请求到达时若已越过允许窗口,不能因为之前有过一次许可,就继续无限期使用旧内容。 对于 max-age=30, stale-if-error=60,额外六十秒从新鲜期结束后开始计算,不是从每一次失败尝试重新开始。候选响应的总年龄窗口约为九十秒。RFC 5861 指定了会形成 500、502、503、504 的错误场景。认证失败或资源不存在,不能随手重新包装成可用性故障,再用旧的成功响应掩盖真正需要客户端看到的结果。 成功的条件验证不一定重新下载正文。三零四响应可以依据验证元数据更新存储响应,同时保留原来的表示字节。应把这种结果与替换正文的二百响应、故障期间的陈旧命中分别记录;三种情况可能显示相同页面,却对应不同的新鲜度依据,不能仅靠正文是否改变判断验证是否发生。 坏掉的样子 每次重验证失败后都把 Age 清零,会把有界例外变成无限陈旧。保留旧正文却刷新存储时间戳,也会造成同类错误。失败时应保留年龄;只有真正的新响应或成功验证,才提供更新新鲜度的依据。后台验证通常由流量触发,所以一个长期无人访问的缓存,不一定会在下一次用户访问之前主动把内容更新好。 只读或幂等并不代表可以安全返回旧内容。一个 GET 也可能涉及对时效敏感的权限或价格。RFC 9111 禁止在适用的 no-cache、must-revalidate 等指令不允许时复用陈旧响应。应避免互相矛盾的策略,并测试实际部署的缓存如何处理这些限制。另外,RFC 5861 中关于旧 Warning 字段的建议,已被废弃该字段的 RFC 9111 更新,不能把它当作当前验收必需项。 观察指标中要区分陈旧响应和新鲜命中,记录总年龄、错误类别以及验证是否成功,同时避免记录秘密信息。如果每次都返回旧内容,漂亮的 HTTP 成功率可能掩盖持续故障的源站。恢复验收必须确认发生了真实替换或成功验证,而不是只看到源站进程重新启动,就宣布数据已经恢复新鲜。 快速验证 准备 Node.js 与 npm。本轮使用 Node 24.19.0 和已有的 http-cache-semantics 4.2.0 安装运行了 JavaScript。安装固定版本后,用 NODE_PATH="$stale_lab/node_modules" node 执行 JavaScript。这是使用虚构响应和受控时钟的确定性策略与正文选择实验,不发送请求,也不制造真实宕机;只有安装阶段需要联网。 ...

二月 1, 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
“重试时保留同一个键”标题卡旁边排列观察、决策与行动三个面板。

幂等键是客户端的承诺

幂等键命名的是一次预期行动,不是一次网络投递尝试。服务支持这项契约时,客户端在结果不明确后重试同一个行动,应该继续发送同一个键。每次重试都生成新键,实际是在告诉服务器“这是另一次行动”,因此完全可能合理地产生第二份业务效果。客户端必须先把行动身份与尝试次数分开。 创建一次,在后续尝试中保留 在第一次发送之前生成键,并把它与操作参数、目标一起保存。如果后台任务可能在客户端重启后恢复,任务记录也需要保留这个身份。仅把键放在临时网络连接或一次调用栈中,无法可靠地把后续尝试关联回原来的意图,恢复程序也不应临时猜一个替代值。 真正的新行动应该使用新键,即使参数与之前的购买完全一样。仅用正文哈希无法区分“再买一次同样的东西”和“重试第一次购买”。键与请求指纹承担不同职责:键标记行动,指纹检查是否发生了不兼容的复用。把二者合并,既可能漏去重,也可能错误阻止合理的新操作。 键的格式和作用域应遵循服务提供方的契约。Stripe 建议使用 UUIDv4 等随机标识,并提醒不要把敏感数据放进键中。键不是认证凭据,拥有某个键也不能代替访问已存结果所需的授权。不要把 bearer 凭据、邮箱或个人信息塞进用于关联请求的标识。 接收方必须真正实现这份承诺 只加一个 Idempotency-Key 请求头,不会自动实现去重。服务端必须在明确契约下识别它,说明比较哪些参数、保留哪些结果、执行尚未完成时如何处理重复请求,以及记录会保存多久。这些都是服务行为,不能从客户端看见一个字段名就推断已经具备。 Stripe API v1 记录的是首次进入执行阶段的请求状态码与正文,包括 500 响应。如果参数验证失败,或与正在执行的请求发生冲突,而端点执行尚未开始,则不会保存对应幂等结果。因此,“重复请求必须等待第一份结果”并不是所有 API 都必须采用的通用规则,调用方要处理实际的在途冲突语义。 Stripe API v2 则可能继续尝试失败或部分失败的工作,并返回更新后的结果;不能把 v1 的原响应保存规则直接套到 v2。不同提供方也不保证逐字重放响应。EC2 文档说明,成功重试不会再次执行行动,但返回结果可以包含更新后的创建状态等信息。RFC 9110 定义的是预期效果;具体服务是否复用原始响应字节,是叠加在其上的另一层契约,不能把两者当成同一要求。 让键的含义保持稳定 同一个键被配上不兼容的参数时,应按照服务明确的错误规则处理,而不是静默创建另一项操作,或覆盖原先保存的结果。JSON 字段顺序、默认值、数字表示和省略字段,都可能影响比较。一个小例子能规范化某种输入,不代表已经解决任意正文的语义等价问题。 下面的实验只接受一个商品字符串和一个正整数量。将这两个字段排序,定义了它自己的比较规则,并不是通用 JSON 等价算法。示例用 409 表示参数冲突,也是测试接口的政策,不是每一家幂等 API 都保证返回的状态码。 ...

一月 20, 2027 · 3 分钟 · XB Field Notes
Accept 与 Vary 标题卡旁边连接着观察、决策与行动三个面板。

Accept 与 Vary 让一个 URL 回应多种格式

同一个资源可以同时提供面向读者的 HTML 视图和面向程序的 JSON 表示。共用 URL 的前提是源站与中间层始终保留这种区别。端点需要选择可用格式、正确序列化、说明缓存变化维度,并验证本次选中的表示,而不是拿同一资源的另一种版本来代替它。单一地址不会自动完成这些工作。 先选择,再序列化与验证 对于 GET 响应,请求中的 Accept 表达希望接收哪些媒体类型,请求中的 Content-Type 则描述请求内容本身。把后者写成 JSON,并不等于要求服务器返回 JSON。质量值与通配符匹配应交给框架已有的媒体范围解析器。应用还需要分别规定:没有 Accept 时默认返回什么,以及字段存在却匹配不到任何可用表示时如何处理。 下面的端点只在 Accept 缺失时默认选择 HTML。如果客户端明确表达的偏好没有可接受候选,就返回 406,并列出可用类型。这是一项刻意选择的接口策略,因为 HTTP 也允许忽略偏好。不要把它换成 415,后者针对的是请求内容格式不受支持。分清错误方向,客户端才能知道应该修改希望接收的类型,还是自己发送的正文及其声明。 完成选择后,Content-Type 应由实际使用的序列化方式确定。HTML 中插入数据需要适当转义,JSON 则应通过 JSON 序列化器生成,而不是塞进 HTML 模板。该路由的各类表示,包括默认表示,都需要 Vary: Accept。但 Vary 不授予存储资格,也不保护个人信息;实验中的公开缓存指令只适用于这份对所有调用者完全相同、没有敏感信息的固定状态记录,不能直接复制到账户接口。 坏掉的样子 只测源站,可能看不出缓存正在把之前的 HTML 发给现在的 JSON 请求。需要通过完整交付路径检查响应头和解析后的正文,并交换请求顺序。新响应补上正确的 Vary,不会自动修复此前已经缓存、却缺失选择元数据的条目。应使用受控测试地址或已获批准的失效流程,不要为了验证一个端点而随意清空共享缓存。 ...

一月 19, 2027 · 2 分钟 · XB Field Notes
浅色网格上显示文章标题,旁侧是依次标注 Observe、Decide、Act 的三个相连方框。

gRPC 用一条 HTTP/2 连接复用多条流

gRPC 的 HTTP/2 传输允许多个 RPC 尝试共享一条连接,同时保留每条流内部的消息顺序。这对短调用和长时间交换都可能有用,但并不意味着每个调用获得了一条无限且完全独立的传输通道。带宽、TCP 交付、连接级流控以及实现中的资源仍然共享,理解这些边界才能准确判断哪些阻塞能够隔离,哪些会影响其他调用。 让调用形式匹配实际工作 一次基于 HTTP/2 的 RPC 尝试占用一条流;重试会发起另一次尝试,而不是把字节重新写入已经结束的流。复用连接能够摊薄建连和 TLS 的成本,但一个 channel 可以管理多条连接,访问不同后端也不会自动共用同一个 TCP 端点。每次调用仍然包含序列化、元数据、帧处理、调度和业务工作,不能把额外成本说成只剩几个帧头。 gRPC 提供单次请求响应、客户端流、服务器流和双向流四类方法。只有双向流允许双方在同一次 RPC 内独立发送消息序列。每个方向的消息顺序得以保留,但不要求每条请求消息都对应一条回复。应当根据应用契约选择调用形式,而不是认为只要采用 gRPC,所有方法就自然具备双向持续推送能力;什么时候读取和写入,仍需应用合理安排。 HTTP/1.1 可以复用持久连接,WebSocket 本身也支持双向消息,二者都不要求每发送一条应用消息就重新握手。因此,不能用虚构的“一次消息一次连接”来证明 gRPC 总是更省。更有意义的比较包括 RPC 契约、生成接口、状态模型和 HTTP/2 多路复用,再结合完整负载和运维约束测量,才能判断实际延迟、内存和连接开销。 分开调用期限与共享传输 截止时间属于某次 RPC,并不会自动成为同一连接上所有流的统一期限。应通过语言支持的上下文机制向下游传递剩余预算;每经过一层就重新领取完整超时,可能让工作持续到调用方早已不愿等待之后。取消还需要业务代码配合停止工作,已经提交的副作用不会因此自动回滚,客户端未收到成功也不等于服务端没有完成修改。 HTTP/2 对 DATA 帧同时实施流级和连接级流控。某条流的窗口耗尽时,只要连接信用仍足够,其他流就可能继续传输。某个方向的连接信用耗尽,则会限制该方向上各条流继续发送 DATA,但并不禁止所有控制帧。接收端实现可以独立补充连接信用,所以一个慢读者并不必然拖到整条连接都没有额度;应观察真实实现如何管理缓冲和更新。 TCP 丢包仍然可能影响多个 HTTP/2 流共享的字节交付。队列、处理器和内存也会在协议流抽象之外造成耦合。应用向库写入成功,可能只是数据进入了库的缓冲,并不能证明已经送达对端或被业务消费。缓冲应当有界,并在实际客户端和服务器库上验证慢读者场景,不能仅凭连接仍然存活就认为吞吐和资源使用都正常。 ...

一月 16, 2027 · 2 分钟 · XB Field Notes