URL Canonicalizer For Cache Keys
Normalize HTTP URLs by removing default ports and fragments, sorting query pairs and dropping tracking parameters for stable cache keys.
Code
const DEFAULT_PORTS = new Map([
["http:", "80"],
["https:", "443"],
]);
export function canonicalizeUrl(input, options = {}) {
if (typeof input !== "string" || input.trim() === "") {
throw new TypeError("input must be a non-empty URL string");
}
const {
stripFragment = true,
sortQuery = true,
dropQuery = [],
keepTrailingSlash = true,
} = options;
if (!Array.isArray(dropQuery)) throw new TypeError("dropQuery must be an array");
const dropped = new Set(dropQuery.map((key) => String(key)));
let url;
try {
url = new URL(input.trim());
} catch {
throw new TypeError("invalid URL: " + input);
}
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new TypeError("only http and https URLs are supported");
}
if (stripFragment) url.hash = "";
if (DEFAULT_PORTS.get(url.protocol) === url.port) url.port = "";
if (url.pathname === "") url.pathname = "/";
if (!keepTrailingSlash && url.pathname.length > 1 && url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (sortQuery) {
const pairs = [...url.searchParams.entries()].filter(([key]) => !dropped.has(key));
pairs.sort((left, right) => {
if (left[0] !== right[0]) return left[0] < right[0] ? -1 : 1;
if (left[1] === right[1]) return 0;
return left[1] < right[1] ? -1 : 1;
});
url.search = "";
for (const [key, value] of pairs) url.searchParams.append(key, value);
}
return url.toString();
}
const urlKeyCache = new Map();
// Memoize canonical results so one logical URL maps to one cache key.
export function urlCacheKey(input, options = {}) {
const fingerprint = [input, options.sortQuery ?? true, options.stripFragment ?? true, options.keepTrailingSlash ?? true, (options.dropQuery ?? []).join(',')].join('|');
let key = urlKeyCache.get(fingerprint);
if (key === undefined) {
key = canonicalizeUrl(input, options);
urlKeyCache.set(fingerprint, key);
}
return key;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.