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 →