功能说明
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,
};
}