← 中文资源

简体中文版本

Content-Type 请求头解析

解析媒体类型与参数,处理带分号的引号值、字符集与重复参数,返回规范化结构。

阅读英文原文

功能说明

parseContentType 把 Content-Type 头拆分为主类型、子类型、标准化的 essence 与参数对象,能正确处理引号内的分号、转义字符与大小写。返回的 charset、boundary 等参数可直接用于后续解码或 multipart 解析。适合上传接口、内容协商、Webhook 接收端和代理转发。解析失败会抛出明确错误,避免把畸形头当作 octet-stream。

关键代码

参数拆分逐字符扫描,只有引号外的分号才作为分隔符,反斜杠在引号内会连同下一个字符一起保留,交给后续反转义。主体按斜杠拆成两段,并用允许字符的正则校验,防止把带空白的垃圾文本当作媒体类型。参数名统一小写,值去掉引号并还原转义,重复参数保留第一个,与常见解析器行为一致。

使用注意

该函数只解析媒体类型,不校验内容是否与声明一致,上传安全仍需魔数检测。参数值大小写不应统一,例如 boundary 区分大小写。缺少引号的参数遇到分号会被提前截断,畸形头应被拒绝而不是猜测。对 multipart 的 boundary,需要限制长度并确保后续解析器不把它当作正则注入。

验证方式

测试 text/html; charset=UTF-8、application/json;charset=utf-8、带引号分号的参数、带转义引号的值与重复 charset。验证主类型和参数名被规范化,boundary 保持原大小写。测试缺少子类型、空头、非法字符、只有分号与多余等号的输入。最后与浏览器实际发送的头对比,确认 essence 与参数拆分结果一致。

代码示例

export interface ContentType {
  type: string;
  subtype: string;
  essence: string;
  parameters: Record<string, string>;
}

function splitParameters(header: string): string[] {
  const parts: string[] = [];
  let current = "";
  let quoted = false;
  for (let index = 0; index < header.length; index += 1) {
    const character = header[index]!;
    if (character === "\\" && quoted && index + 1 < header.length) {
      current += character + header[index + 1];
      index += 1;
      continue;
    }
    if (character === '"') quoted = !quoted;
    if (character === ";" && !quoted) {
      parts.push(current);
      current = "";
      continue;
    }
    current += character;
  }
  parts.push(current);
  return parts;
}

export function parseContentType(header: string): ContentType {
  if (typeof header !== "string" || header.trim() === "") {
    throw new TypeError("header must be a non-empty string");
  }
  const [essencePart, ...parameterParts] = splitParameters(header);
  const [type, subtype] = (essencePart ?? "").split("/");
  const token = /^[a-z0-9!#$&^_.+*-]+$/i;
  if (!type || !subtype || !token.test(type) || !token.test(subtype)) {
    throw new SyntaxError("invalid media type: " + essencePart);
  }
  const parameters: Record<string, string> = {};
  for (const part of parameterParts) {
    const separator = part.indexOf("=");
    if (separator <= 0) continue;
    const name = part.slice(0, separator).trim().toLowerCase();
    let value = part.slice(separator + 1).trim();
    if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
      value = value.slice(1, -1).replace(/\\(.)/g, "$1");
    }
    if (!(name in parameters)) parameters[name] = value;
  }
  return {
    type: type.toLowerCase(),
    subtype: subtype.toLowerCase(),
    essence: type.toLowerCase() + "/" + subtype.toLowerCase(),
    parameters,
  };
}