sdxc

Type to search, or start from one of these:

[ HTTP & responses ]

@sdxc/link-header

Parse and stringify RFC 8288 Link headers

npm add @sdxc/link-header
pnpm add @sdxc/link-header
yarn add @sdxc/link-header
bun add @sdxc/link-header
Used by
uptime, blog

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.

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

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.

Joins link-value texts, or the raw of parsed entries, into one header value. An empty list answers null.

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

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.

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.

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.