摘要

  • WebDAV binding 让多个 URI 指向同一个资源。若目标是集合,第二条 binding 会让同一批成员从另一条路径出现,也可能形成回到祖先的环。
  • 对理解 binding 的 Depth: infinity PROPFIND,服务器以 200 展开同一集合的一次出现;后续路径仍作为 DAV:response 存在,但以 208 Already Reported 标记,并省略已经列过的后代。
  • DAV:resource-id 把稳定的资源身份放在多变路径之下。正确客户端既不把每条 URI 当成新资源,也不因资源相同而删掉别名;不理解 208 的客户端遇到环时应得到 508,而不是一份无法解释缺口的“成功”清单。

两个门牌背后可以是同一间库房

一座档案馆可以从东廊与西廊各开一扇门,两扇门都通往同一间库房。门牌是两项真实的导航关系,库房却只有一间。撤掉一扇门会改变路径,但只要另一扇门仍在,库房就不能随之被销毁。

现在让机器从每一扇门进入,递归登记库房里的全部柜子。若它只认路径,同一批柜子会出现两遍。若库房内部有一条通道回到上层走廊,它可能永远登记下去。若机器发现同一库房后干脆删掉第二扇门,清单虽然有限,却不再忠于建筑。

208 给出了第四种做法:把第二扇门写进清单,证明门后是已经展开过的库房,然后停止重复下钻。

它压缩的是遍历工作,不是名字空间事实。

WebDAV 的集合原本就是映射集合

2007 年的 RFC 4918 把 WebDAV collection 的状态描述为路径片段与资源之间的映射,以及集合自身的属性。成员名不是资源本体,而是抵达资源的关系之一。

PROPFIND 可以使用 Depth 0、1 或 infinity。Depth: infinity 要求查看请求集合及其全部后代。结果由外层 207 Multi-Status 承载,正文是一组扁平的 DAV:response:每项以 href 给出路径,再用 propstat 内的状态说明属性结果。

所以 207 从一开始就不是“一切成功”的总判决。它只告诉客户端:不同成员、不同属性的细账在正文里。接收方必须逐项阅读,而不能把外层 2xx 当成内部结论。

RFC 4918 也承认一个资源可能由多个 URI 标识。不过,让客户端主动为现有资源建立新访问路径,是后来 binding 扩展补上的能力。

BIND 新建的是关系,不是副本

2010 年 4 月发布的 RFC 5842 是一份 Experimental RFC。它为 WebDAV 增加 BIND、UNBIND 与 REBIND。BIND 把集合中的一个新路径片段关联到已有资源,让请求可以通过新 URI 抵达原对象。

资源没有被复制。新增的是 binding。即使两个不同集合都以同一个片段名指向同一资源,那也是两条各自存在于集合状态中的关系。

这一点决定了删除语义。移除一条 binding 不得使其他 binding 变成悬空路径,也不得在仍有路径抵达资源时回收资源。Binding integrity 要求这条关系持续存在、持续指向同一资源,直到某个明确定义的方法改变或删除它。

别名不是可以随意丢弃的显示文本。它是有独立生命周期的边。

集合别名会把整棵子树投影到新前缀下

当 binding 指向普通叶子资源时,它多给资源一个 URI。指向集合时,效果会沿成员关系传播:集合里的后代立即可以在新路径前缀下访问,即便服务器没有为每个后代新建 binding。

这意味着一份集合状态可以产生多组 URI mapping。路径数量与资源数量不再同步。

若集合绑定到自身,或者绑定回某个祖先,映射会出现 /self、/self/self、/self/self/self 的无穷序列。服务器里并没有无穷多个文件;无穷的是沿同一关系反复生成的路径。

RFC 5842 要求服务器在处理 Depth: infinity 时检测由集合 binding 形成的环。服务器也可以一开始拒绝会制造环的请求,因为支持环不是强制能力。只要允许环存在,递归器就不能再假装面对普通目录树。

路径身份与资源身份必须分账

比较 URI 无法识别共享资源,因为两条不同 URI 本来就可能指向同一对象。比较内容同样不够:两个不同资源完全可能拥有相同字节与属性,之后却独立变化。

RFC 5842 因此要求 DAV:resource-id。资源创建时获得这个值;它在所有资源之间永久唯一;PUT 更新现有资源或 REBIND 移动路径时不得改变;即使资源后来没有任何 URI 可达,标识也不得重新分配给其他资源。

客户端若从两条 binding 读取到逐字符相同的 resource-id,就能证明两条路径抵达同一资源。这个标识不是“首选网址”,而是位于网址之下、不随路径增减而漂移的对象身份。

208 的省略因此可以被验证。客户端知道被跳过的是已经展开过的同一集合,而不是一个外观相似的新集合。

深度遍历需要两本账

只用 URI 作为 visited key,会完整记录路径,却可能重复处理同一集合,在环中永不结束。只用 resource-id 去重,可以阻止重复展开;但若在记录 binding 之前就合并对象,第二条合法路径会从名字空间消失。

忠实的图遍历需要两本账。第一本记“有哪些边”:每个集合、路径片段与目标资源之间的 binding。第二本记“哪些对象已展开”:在本次响应中,哪个集合身份的后代已经列过。

208 是两本账之间的协议连接。它告诉客户端:把这条 href 留在边账里;至于为何后面没有子项,请查展开账,这个 resource-id 已经完成过一次。

这样,资源不会因路径增多而膨胀,路径也不会因资源相同而消失。

208 省略后代,不省略第二条路径

它的规范范围很窄:在 Depth: infinity 请求中,服务器可以把 208 放进 DAV:propstat。同一集合在请求范围内有多条 binding 时,其中一次以 200 报告并展开;其他 binding 仍有自己的 DAV:response,但状态为 208,且不再为其后代生成 response 项。

第二条 binding 本身必须可见。它的 href 保留路径,客户端可以同时请求 DAV:resource-id,把这个路径连接到先前已经展开的集合节点。

RFC 的示例里,/Coll/ 与 /Coll/Bar 返回相同 resource-id。前者以 200 出现,其成员被列出;后者以 208 出现。响应不会继续制造 /Coll/Bar/Bar/Bar,因为每一次都只是回到同一个集合。

Already Reported 说的是本份 Multi-Status 中的展开历史。它没有说第二条 URI 无效,也没有授权服务器删掉这条边。

第一个 200 不是加冕典礼

哪个路径以 200 承担展开,哪个路径以 208 引用结果,是一次响应中的工作分配。RFC 5842 没有因此把前者变成永久 canonical URI,更没有赋予它处置其他 binding 的所有权。

两条 binding 都是集合状态。服务器为了让响应有限,必须选一次出现来列后代;这个选择不改变其他路径的法律或操作现实。

如果客户端把 200 路径认作“真正对象”,把 208 路径当作可以清除的重复,它会丢失父集合归属、访问路径、审计记录甚至授权上下文。随后一次 UNBIND 也可能被误解为删除资源,而不是只撤销一条关系。

遍历先后不能悄悄升级为名字空间权力。

207 仍然是外层信封

请求级响应通常仍是 207 Multi-Status。208 出现在正文某个资源条目的属性状态中。

外层 207 告诉接收方:不要从 status line 推断全部细节。内层 200 与 208 说明各条 binding 如何参与这次遍历。href 记住路径,resource-id 连接同一对象,后代项只跟在实际展开的那一次出现之后。

因此,208 不是“你重复发送了 HTTP 请求”,也不是“整个请求早已处理”。它只说明这一条 binding 指向的集合,已经通过本响应中的另一条 binding 报告过后代。

若中间层或客户端只保留外层 207,再把 XML 细账扁平化成一个成功布尔值,有限图遍历的全部证据都会丢失。

省略只有在双方理解时才安全

缺少后代既可能表示“已经在别处列过”,也可能表示“服务器没返回”。若客户端不懂 208,它无法区分这两种情况。

因此,支持 binding 的服务器通过 OPTIONS 响应里的 DAV: bind 能力类声明支持。客户端也应在请求的 DAV 头字段里发送 bind,表示自己理解扩展,尤其理解 208。

为了向后兼容,若客户端没有发出能力信号,服务器不应在 Multi-Status 里使用 208。旧客户端可能把未知状态当作属性失败,也可能保留路径却误以为这个集合没有成员。

这说明 208 不是服务器私下做的输出压缩。它是一项经过协商的省略:接收方知道少掉的内容在哪里、为什么少、如何由稳定身份补回结构。

508 是终止,不是更严厉的 208

不理解 binding 的客户端做深度遍历并遇到环时,RFC 5842 选择 508 Loop Detected,而不是把 208 塞进一份客户端无法解释的成功响应。

若服务器在开始输出前发现环,可以直接返回顶层 508;若 Multi-Status 已经开始流式输出,规范也讨论了在细账中表达 508。核心语义不变:服务器因无穷循环终止操作,整个操作失败。

208 的控制结果正相反。理解 binding 的客户端仍能得到有限而完整的图:第二条边保留,重复后代停止,其他分支继续。508 则明确说这次遍历不能安全完成。

显式失败可能损失一部分可用结果,却好过让旧客户端接收一份看似成功、实际无法理解省略规则的树。

图表达能力也带来攻击面

BIND 让客户端有了制造环的新入口,可能是意外,也可能是恶意。服务器必须在 depth-infinity 处理期间检测环,不能把递归资源耗尽当作客户端自己的问题。

规范还指出跨服务器 binding 的隐私与拒绝服务风险:可信站点上的新路径可能把流量引向原本不承受该负载的目标。可选的 DAV:parent-set 能列出资源的父 binding,却可能泄露私有目录位置,也可能在其他管理域不断增加 binding 时成为维护成本攻击面。

208 只解决一次响应内的重复展开。它没有规定完整的 CPU、内存、边数或响应大小预算,也没有替代授权。有限的图仍可能很大,准确的路径仍可能敏感。

薄协议只承担它能证明的那一段,不把一项防重复机制包装成全面安全方案。

同一资源不保证所有路径观察完全相同

resource-id 相同证明对象相同,却不意味着所有属性在不同路径下必须返回同样的值。RFC 5842 规定 dead property 不随 binding 数量或提交路径变化;live property 则取决于各自定义,其中有的可能具有路径相关行为。

客户端如果仅凭 resource-id 合并全部观察,会把路径语境压平;如果因为属性值不同就认定资源不同,又会否定稳定身份的证据。

合理模型应区分资源事实、binding 事实与路径相关观察。三者可以联系,却不能彼此吞并。

这也是 208 背后的制度边界:共享的最小身份负责协作,本地路径保留自己的事实,不让任何一层越权解释其他层。

IANA 注册的是一项实验性扩展的精确状态

当前 IANA HTTP 状态码注册表 把 208 Already Reported 指向 RFC 5842;同一 RFC 还定义了 508,207 则指向 Standards Track 的 RFC 4918。

注册没有把 RFC 5842 变成 Internet Standard,也不能证明当下哪些产品实现了它。208 仍属于 binding-aware WebDAV、Depth infinity 与 Multi-Status 的特定组合。

它不是数据库重复行标记,不是幂等收据,不是缓存验证器,也不是所有图 API 都应借用的通用状态。

窄语义正是它能长期被准确理解的原因。

每条路径都报告,同一集合只展开一次

208 没有把图强行画回树,也没有在“无限重复”与“删除别名”之间二选一。

每条 binding 都必须报告,因为路径是实际的名字空间状态。resource-id 必须稳定,因为对象不等于某一条路径。集合后代只展开一次,因为第二次不会增加资源结构。省略处必须标 208,因为没有解释的缺口与丢数据无法区分。

最终形成的是一种克制的共同层:它不宣布哪条路径是正统,不把别名误作副本,也不让递归器因自身模型过薄而制造无穷对象。

第二扇门仍画在平面图上。门后的同一间库房,无须再次清点。