← 中文资源

简体中文版本

ETag 强弱比较与条件请求匹配

解析实体标签列表,按 HTTP 规范以强比较或弱比较评估 If-Match 与 If-None-Match。

阅读英文原文

功能说明

该示例处理 HTTP 条件请求中的 ETag 比较。实体标签可能是强标签(带双引号)或弱标签(W/ 前缀),If-None-Match 使用弱比较,只要 opaque 部分相同即视为匹配;If-Match 使用强比较,两侧都必须不是弱标签。通配符星号表示只要当前存在表示就匹配。常用于静态资源协商缓存、乐观并发控制与避免重复下载。

关键代码

parseETagList 逐字符扫描而不是简单按逗号分割,因为 opaque 标签内部允许出现逗号;扫描时跳过字段间空白,识别可选的 W/ 前缀,再读取到下一个双引号,遇到未闭合或缺少引号立即抛 SyntaxError。strongMatch 额外要求两边都不是弱标签,weakMatch 只比较 opaque。两个条件函数分别遍历解析结果,星号分支单独处理,避免把星号当作普通标签。

使用注意

比较是大小写敏感的,opaque 值不会做解码或规范化。函数只接受字符串,缺少头部时应由调用方传空串或直接判断,空串会解析为空列表并返回 false。损坏的头部选择抛错而不是静默忽略,便于在代理或测试中尽早发现问题。弱比较不能用于需要强一致性的场景,例如断点续传或写操作的 If-Match。

验证方式

构造包含逗号的标签,确认解析不被拆分;用同一 opaque 分别生成强、弱标签,验证 strongMatch 为 false、weakMatch 为 true;测试 If-None-Match 能匹配弱标签而 If-Match 不能;检查星号在 current 为空时返回 false;最后验证缺少引号与未闭合标签会抛 SyntaxError。

代码示例

export function parseETagList(header) {
  if (typeof header !== "string") throw new TypeError("header must be a string");
  const tags = [];
  let index = 0;
  while (index < header.length) {
    while (index < header.length && (header[index] === "," || /\s/.test(header[index]))) {
      index += 1;
    }
    if (index >= header.length) break;
    let weak = false;
    if (header.startsWith("W/", index)) {
      weak = true;
      index += 2;
    }
    if (header[index] !== '"') throw new SyntaxError("invalid ETag at index " + index);
    index += 1;
    const start = index;
    while (index < header.length && header[index] !== '"') index += 1;
    if (index >= header.length) throw new SyntaxError("unterminated ETag");
    const opaque = header.slice(start, index);
    index += 1;
    tags.push({ weak, opaque });
  }
  return tags;
}

export function strongMatch(left, right) {
  return Boolean(left && right && !left.weak && !right.weak && left.opaque === right.opaque);
}

export function weakMatch(left, right) {
  return Boolean(left && right && left.opaque === right.opaque);
}

export function matchesIfNoneMatch(header, current) {
  if (typeof header !== "string") throw new TypeError("header must be a string");
  if (header.trim() === "*") return current != null;
  return parseETagList(header).some((tag) => weakMatch(tag, current));
}

export function matchesIfMatch(header, current) {
  if (typeof header !== "string") throw new TypeError("header must be a string");
  if (header.trim() === "*") return current != null;
  return parseETagList(header).some((tag) => strongMatch(tag, current));
}