Link Header Parser For RFC 8288 Relations
Parse Link headers into URLs, relation arrays and typed parameters, handling quoted commas, relative references and multiple links.
Code
export interface ParsedLink {
url: string;
rel: string[];
params: Record<string, string>;
}
function skipWhitespace(header: string, index: number): number {
let cursor = index;
while (cursor < header.length && /\s/.test(header[cursor])) cursor += 1;
return cursor;
}
function parseParameters(
header: string,
start: number,
): { params: Record<string, string>; next: number } {
const params: Record<string, string> = {};
let index = start;
while (index < header.length && header[index] !== ",") {
index = skipWhitespace(header, index);
if (index >= header.length || header[index] === ",") break;
if (header[index] !== ";") throw new SyntaxError("expected ';' at index " + index);
index = skipWhitespace(header, index + 1);
const keyStart = index;
while (index < header.length && /[A-Za-z0-9!#$%&'*+\-.^_\x60|~]/.test(header[index])) {
index += 1;
}
if (index === keyStart) throw new SyntaxError("missing link parameter name");
const key = header.slice(keyStart, index).toLowerCase();
index = skipWhitespace(header, index);
if (header[index] !== "=") throw new SyntaxError("missing '=' for " + key);
index = skipWhitespace(header, index + 1);
let value = "";
if (header[index] === '"') {
index += 1;
let closed = false;
while (index < header.length) {
const character = header[index];
if (character === "\\") {
index += 1;
if (index >= header.length) throw new SyntaxError("unterminated escape");
value += header[index];
index += 1;
} else if (character === '"') {
index += 1;
closed = true;
break;
} else {
value += character;
index += 1;
}
}
if (!closed) throw new SyntaxError("unterminated quoted parameter");
} else {
const valueStart = index;
while (index < header.length && header[index] !== ";" && header[index] !== ",") {
index += 1;
}
value = header.slice(valueStart, index).trim();
if (value === "") throw new SyntaxError("empty link parameter value");
}
params[key] = value;
}
return { params, next: index };
}
export function parseLinkHeader(header: string, baseUrl?: string): ParsedLink[] {
if (typeof header !== "string") throw new TypeError("header must be a string");
const links: ParsedLink[] = [];
let index = 0;
while (index < header.length) {
index = skipWhitespace(header, index);
while (header[index] === ",") index = skipWhitespace(header, index + 1);
if (index >= header.length) break;
if (header[index] !== "<") throw new SyntaxError("expected '<' at index " + index);
const end = header.indexOf(">", index + 1);
if (end === -1) throw new SyntaxError("unterminated link target");
const target = header.slice(index + 1, end).trim();
if (target === "") throw new SyntaxError("empty link target");
index = end + 1;
const { params, next } = parseParameters(header, index);
index = next;
if (header[index] === ",") index += 1;
let url = target;
if (baseUrl !== undefined) {
try {
url = new URL(target, baseUrl).toString();
} catch {
throw new SyntaxError("invalid link URL: " + target);
}
}
links.push({
url,
rel: (params.rel ?? "").split(/\s+/).filter((entry) => entry.length > 0),
params,
});
}
return links;
}
export function findLink(links: readonly ParsedLink[], rel: string): string | undefined {
return links.find((link) => link.rel.includes(rel))?.url;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.