Skip to content

URL Inspector — TypeScript source

Break any URL into its components - protocol, host, port, path, query params, hash, and credentials. Detects default ports and security at a glance, with a decode toggle for query values. Runs entirely in your browser.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// WHATWG URL inspector. Wraps the platform URL parser in a pure, never-throwing
// function and exposes a flat, serialisable report of every URL component -
// including the signals the URL API hides (credentials, default-vs-explicit
// ports, root-only/fragment-only URLs).

export interface UrlParam {
  key: string;
  value: string;
}

export interface UrlReport {
  valid: boolean;
  protocol?: string;
  username?: string;
  password?: string;
  host?: string;
  hostname?: string;
  port?: string;
  pathname?: string;
  search?: string;
  hash?: string;
  searchParams: UrlParam[];
  origin?: string;
  isSecure?: boolean;
  defaultPort?: boolean;
  warnings: string[];
}

// Well-known default ports per scheme (the URL API normalises these away).
const DEFAULT_PORTS: Record<string, string> = {
  'http:': '80',
  'https:': '443',
  'ftp:': '21',
  'ws:': '80',
  'wss:': '443',
};

/** Percent-decode a query value, treating '+' as a space; never throws. */
export function decodeParam(v: string): string {
  try {
    return decodeURIComponent(v.replace(/\+/g, ' '));
  } catch {
    return v; // malformed percent-encoding - return the original
  }
}

/**
 * Read an explicitly-written port straight from the raw input. The WHATWG URL
 * API normalises default ports (https:443 → '') away, so we re-parse the
 * authority to recover them. Handles userinfo (`user:pass@`) and IPv6 literals
 * (`[::1]:8080`). Returns undefined when no port is present or it is non-numeric.
 */
function rawPort(trimmed: string): string | undefined {
  const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(.*)$/s);
  if (!schemeMatch) return undefined;
  const rest = schemeMatch[2];
  const authorityEnd = rest.search(/[/?#]/);
  const authority = authorityEnd === -1 ? rest : rest.slice(0, authorityEnd);
  const atIdx = authority.lastIndexOf('@');
  const hostport = atIdx === -1 ? authority : authority.slice(atIdx + 1);

  let portCandidate: string | undefined;
  if (hostport.startsWith('[')) {
    const closeBracket = hostport.indexOf(']');
    if (closeBracket === -1) return undefined;
    const tail = hostport.slice(closeBracket + 1);
    if (tail.startsWith(':')) portCandidate = tail.slice(1);
  } else {
    const colon = hostport.indexOf(':');
    if (colon !== -1) portCandidate = hostport.slice(colon + 1);
  }
  if (portCandidate === undefined) return undefined;
  return /^\d+$/.test(portCandidate) ? portCandidate : undefined;
}

/** Parse and decompose a URL into a structured report; never throws. */
export function inspectUrl(raw: string): UrlReport {
  const warnings: string[] = [];
  const trimmed = (raw ?? '').trim();

  if (!trimmed) {
    return { valid: false, warnings: ['URL is empty'] };
  }

  let url: URL;
  try {
    url = new URL(trimmed);
  } catch {
    return { valid: false, warnings: ['Invalid URL - could not be parsed (include the scheme, e.g. https://)'] };
  }

  const searchParams: UrlParam[] = [];
  url.searchParams.forEach((value, key) => searchParams.push({ key, value }));

  if (url.username) warnings.push('URL contains a username credential');
  if (url.password) warnings.push('URL contains a password credential');

  const explicitPort = rawPort(trimmed);
  let isDefaultPort: boolean | undefined = undefined;
  if (explicitPort !== undefined) {
    const expected = DEFAULT_PORTS[url.protocol];
    isDefaultPort = expected ? explicitPort === expected : false;
    if (isDefaultPort) {
      warnings.push(`Port ${explicitPort} is the default for ${url.protocol}`);
    }
  }

  if (url.pathname === '/' && !url.search && searchParams.length === 0) {
    warnings.push('URL points to the site root (no path or query)');
  }

  const isSecure = url.protocol === 'https:' || url.protocol === 'wss:';

  return {
    valid: true,
    protocol: url.protocol,
    username: url.username || undefined,
    password: url.password || undefined,
    host: url.host,
    hostname: url.hostname,
    port: explicitPort,
    pathname: url.pathname,
    search: url.search || undefined,
    hash: url.hash || undefined,
    searchParams,
    origin: url.origin !== 'null' ? url.origin : undefined,
    isSecure,
    defaultPort: isDefaultPort,
    warnings,
  };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →