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.

JavascriptData Integrity

Code

snippet
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;
}