Raw HTTP Header Block Parser
Parse a raw HTTP header block with obsolete line folding, duplicate fields, Set-Cookie arrays, case-insensitive names and size limits.
Code
const HEADER_NAME = /^[!#$%&'*+\-.^_\x60|~0-9a-z]+$/;
export class HeaderBlock {
#fields = new Map();
add(name, value) {
const key = name.toLowerCase();
if (!HEADER_NAME.test(key)) throw new SyntaxError("invalid header name: " + name);
const current = this.#fields.get(key);
if (current === undefined) {
this.#fields.set(key, value);
} else if (key === "set-cookie") {
const list = Array.isArray(current) ? current : [current];
this.#fields.set(key, [...list, value]);
} else {
this.#fields.set(key, current + ", " + value);
}
}
append(name, value) {
const key = name.toLowerCase();
const current = this.#fields.get(key);
if (current === undefined) throw new SyntaxError("continuation line without a header");
if (Array.isArray(current)) {
const last = current[current.length - 1];
this.#fields.set(key, [...current.slice(0, -1), last + " " + value]);
} else {
this.#fields.set(key, current + " " + value);
}
}
get(name) {
const value = this.#fields.get(name.toLowerCase());
if (value === undefined) return null;
return Array.isArray(value) ? value.join(", ") : value;
}
getAll(name) {
const value = this.#fields.get(name.toLowerCase());
if (value === undefined) return [];
return Array.isArray(value) ? [...value] : [value];
}
has(name) {
return this.#fields.has(name.toLowerCase());
}
names() {
return [...this.#fields.keys()];
}
toObject() {
const output = Object.create(null);
for (const [name, value] of this.#fields) {
output[name] = Array.isArray(value) ? [...value] : value;
}
return output;
}
}
export function parseHeaderBlock(raw, options = {}) {
if (typeof raw !== "string") throw new TypeError("raw headers must be a string");
const { maxLines = 200, maxBytes = 64 * 1024 } = options;
if (!Number.isInteger(maxLines) || maxLines < 1) {
throw new RangeError("maxLines must be a positive integer");
}
if (!Number.isInteger(maxBytes) || maxBytes < 1) {
throw new RangeError("maxBytes must be a positive integer");
}
const byteLength = new TextEncoder().encode(raw).length;
if (byteLength > maxBytes) throw new RangeError("header block exceeds " + maxBytes + " bytes");
const lines = raw.replace(/\r\n?/g, "\n").split("\n");
const headers = new HeaderBlock();
let lastName = null;
let lineCount = 0;
for (const line of lines) {
if (line === "") break;
lineCount += 1;
if (lineCount > maxLines) throw new RangeError("header block exceeds " + maxLines + " lines");
if (/^[ \t]/.test(line)) {
if (lastName === null) throw new SyntaxError("continuation line without a header");
headers.append(lastName, line.trim());
continue;
}
const colon = line.indexOf(":");
if (colon <= 0) throw new SyntaxError("invalid header line: " + line);
const name = line.slice(0, colon).trim();
const value = line.slice(colon + 1).trim();
headers.add(name, value);
lastName = name.toLowerCase();
}
return headers;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.