[ HTTP & responses ]
@sdxc/link-header
Parse and stringify RFC 8288 Link headers
- Used by
- uptime, blog
- Source
- packages/link-header
Parse and stringify RFC 8288 Link headers.
A Link value is a
list of URI-References with relation types and parameters. Splitting on commas is wrong: a
target can contain a comma and a parameter value can be quoted, so this package scans angle
brackets and quoted strings instead. Malformed entries are dropped so a merge into a response
already under construction cannot fail on one bad link.
Installation
npm add @sdxc/link-header
Usage
Parse a header
import { parse } from "@sdxc/link-header";
let links = parse('</a,b.css>; rel="preload"; as="style", </items?page=2>; rel="next"');
links[0]?.target; // "/a,b.css"
links[0]?.rels; // ["preload"]
links[0]?.params; // { as: "style" }
links.find((link) => link.rels.includes("next"))?.target; // "/items?page=2"
parse(null) is []. A type parameter lands in params.type, which is how a feed reader
learns the advertised media type.
Write a header
import { serializeLink, stringify } from "@sdxc/link-header";
stringify([
serializeLink("https://api.example.com/items?page=2", "next"),
serializeLink("/feed.xml", "self", { type: "application/atom+xml" }),
]);
// <https://api.example.com/items?page=2>; rel="next", </feed.xml>; rel="self"; type="application/atom+xml"
stringify([]) is null, so the caller deletes the header instead of writing an empty one.
Merge without dropping foreign links
import { parse, serializeLink, stringify } from "@sdxc/link-header";
let kept = parse(headers.get("Link"))
.filter((link) => !link.rels.includes("next"))
.map((link) => link.raw);
let value = stringify([...kept, serializeLink(nextUrl, "next")]);
if (value === null) headers.delete("Link");
else headers.set("Link", value);
raw is the exact source text, so parameters this caller did not read still round-trip.
API
parse(header: string | null): Link[]
Splits a Link header into entries in source order, per
RFC 8288. A null or empty header answers []. An
entry with no URI-Reference is skipped. The call never throws.
stringify(links: readonly Link[] | readonly string[]): string | null
Joins link-value texts, or the raw of parsed entries, into one header value. An empty list
answers null.
serializeLink(target: string, rel: string, params?: Record<string, string>): string
Renders one link-value with a quoted rel and any extra parameters after it. Values are quoted
so a comma inside one cannot split the header.
Types
Link
A parsed link-value. target is the URI-Reference with angle brackets stripped. rels is the
first rel parameter, lowercased and split on whitespace. params is every other named
parameter, first of each name, unquoted. raw is the exact source text for a round-trip merge.
Pattern: Follow Next Links As A Client
Walk a paginated API until rel="next" runs out.
import { parse } from "@sdxc/link-header";
export async function fetchAll<T>(start: string): Promise<T[]> {
let items: T[] = [];
let next: string | null = start;
while (next !== null) {
let response = await fetch(next);
items.push(...((await response.json()) as T[]));
let links = parse(response.headers.get("Link"));
next = links.find((link) => link.rels.includes("next"))?.target ?? null;
}
return items;
}
Relative targets stay relative; resolve them with new URL(target, response.url) when the
resource was fetched.