Skip to content

URL Inspector — JavaScript 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 JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * url-inspector - JavaScript polyglot showcase port.
 *
 * Wraps the platform WHATWG 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).
 *
 * Display source - part of CosmoDev's polyglot tool pages.
 * Ported from src/lib/url-inspector.ts (the canonical TypeScript lib).
 *
 * JavaScript is the closest sibling to the TypeScript original: it leans on
 * the same platform `URL` / `URLSearchParams` globals, so the WHATWG
 * normalisation (lowercased host, default-port elision, opaque-origin "null",
 * path canonicalisation) is identical for free. The port drops the type
 * annotations and uses modern ES module syntax.
 */

/**
 * Well-known default ports per scheme. The WHATWG URL API normalises these
 * away (https:443 → ""), so we keep the table to detect when an *explicitly
 * written* port is in fact the scheme default.
 */
const DEFAULT_PORTS = {
  'http:': '80',
  'https:': '443',
  'ftp:': '21',
  'ws:': '80',
  'wss:': '443',
};

/**
 * Percent-decode a query value, treating '+' as a space; never throws.
 *
 * `decodeURIComponent` is the WHATWG-aligned decoder, but unlike
 * `URLSearchParams` it does NOT convert '+', so we do that first by hand.
 * Malformed percent-encoding (e.g. a stray '%') would make it throw - we
 * swallow that and return the original string verbatim.
 */
export function decodeParam(v) {
  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 to
 * report "port 443 is the default" we have to re-parse the authority segment
 * ourselves. Handles userinfo (`user:pass@`) and IPv6 literals (`[::1]:8080`).
 *
 * Returns `undefined` when no port is present or what follows the colon is
 * non-numeric.
 */
function rawPort(trimmed) {
  // Scheme must start with a letter, then alnum / '+' / '-' / '.', then "://".
  // [\s\S]* matches the "rest" including newlines (the TS used the /s flag).
  const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([\s\S]*)$/);
  if (!schemeMatch) return undefined;

  const rest = schemeMatch[2];
  // The authority runs until the first path/query/fragment delimiter.
  const authorityEnd = rest.search(/[/?#]/);
  const authority = authorityEnd === -1 ? rest : rest.slice(0, authorityEnd);

  // Drop userinfo: everything up to the LAST '@' belongs to credentials.
  const atIdx = authority.lastIndexOf('@');
  const hostport = atIdx === -1 ? authority : authority.slice(atIdx + 1);

  let portCandidate;
  if (hostport.startsWith('[')) {
    // IPv6 literal - the port (if any) lives after the closing bracket.
    const closeBracket = hostport.indexOf(']');
    if (closeBracket === -1) return undefined; // unterminated bracket
    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) {
  const warnings = [];
  const trimmed = (raw ?? '').trim();

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

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

  // Preserve insertion order AND duplicates (URLSearchParams.forEach does both).
  const searchParams = [];
  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');

  // Recover the explicitly-written port and check it against the scheme default.
  const explicitPort = rawPort(trimmed);
  let isDefaultPort;
  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,
    // WHATWG yields the literal string "null" for opaque origins (file:, data:…).
    origin: url.origin !== 'null' ? url.origin : undefined,
    isSecure,
    defaultPort: isDefaultPort,
    warnings,
  };
}

export { DEFAULT_PORTS };

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 →