URL Inspector — C# 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 C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// url-inspector — break any URL into components (protocol, credentials, host, port, path, query params, fragment), detecting default ports and security at a glance. C# (.NET 7+) port of src/lib/url-inspector.ts — same logic as this dir's javascript.js; System.Uri fills Port with the scheme default even when nothing was written, so the explicitly-written port is recovered by re-parsing the authority (userinfo + IPv6 aware).
using System;
using System.Collections.Generic;
namespace UrlInspector;
/// A single decoded query parameter, in insertion order (duplicates preserved).
public record UrlParam(string Key, string Value);
/// Flat decomposition; null mirrors the TS undefined-when-absent fields.
public sealed class UrlReport {
public bool Valid { get; init; }
public List<string> Warnings { get; init; } = new();
public string? Protocol { get; init; }
public string? Username { get; init; }
public string? Password { get; init; }
public string? Host { get; init; }
public string? Hostname { get; init; }
public string? Port { get; init; }
public string? Pathname { get; init; }
public string? Search { get; init; }
public string? Hash { get; init; }
public List<UrlParam> SearchParams { get; init; } = new();
public string? Origin { get; init; }
public bool IsSecure { get; init; }
public bool? DefaultPort { get; init; }
public static UrlReport Invalid(string warning) =>
new() { Valid = false, Warnings = { warning } };
}
public static class UrlInspector {
private const string InvalidMsg =
"Invalid URL - could not be parsed (include the scheme, e.g. https://)";
/// Well-known default ports per scheme, keyed WHATWG-style with the ':'.
private static readonly Dictionary<string, string> DefaultPorts = new() {
["http:"] = "80", ["https:"] = "443", ["ftp:"] = "21", ["ws:"] = "80", ["wss:"] = "443",
};
/// Schemes that yield a non-opaque origin (others serialise origin as "null").
private static readonly HashSet<string> OriginSchemes = new() { "http", "https", "ws", "wss", "ftp" };
/// Percent-decode a query value, '+' as a space. UnescapeDataString never
/// throws and passes malformed escapes through — same outcome as the TS
/// try/catch fallback.
public static string DecodeParam(string v) => Uri.UnescapeDataString(v.Replace('+', ' '));
/// Digits-only check for the raw port candidate.
private static bool DigitsOnly(string s) {
foreach (var c in s) if (!char.IsAsciiDigit(c)) return false;
return s.Length > 0;
}
/// Read an explicitly-written port straight from the raw input: System.Uri
/// normalises it away, so the authority segment is re-parsed by hand.
/// Handles userinfo ("user:pass@") and IPv6 literals ("[::1]:8080").
private static string? RawPort(string trimmed) {
int sep = trimmed.IndexOf("://", StringComparison.Ordinal);
if (sep < 1) return null;
for (int i = 0; i < sep; i++) { // scheme: letter, then alnum / '+' / '-' / '.'
char c = trimmed[i];
bool ok = i == 0 ? char.IsLetter(c)
: char.IsLetterOrDigit(c) || c == '+' || c == '-' || c == '.';
if (!ok) return null;
}
string rest = trimmed[(sep + 3)..];
int end = rest.AsSpan().IndexOfAny('/', '?', '#');
string authority = end < 0 ? rest : rest[..end];
// Drop userinfo: everything up to the LAST '@' belongs to credentials.
int at = authority.LastIndexOf('@');
string hostPort = at < 0 ? authority : authority[(at + 1)..];
string? candidate = null;
if (hostPort.StartsWith('[')) { // IPv6 literal — port lives after ']'
int close = hostPort.IndexOf(']');
if (close < 0) return null; // unterminated bracket
string tail = hostPort[(close + 1)..];
if (tail.StartsWith(':')) candidate = tail[1..];
} else if (hostPort.IndexOf(':') is int colon && colon >= 0) {
candidate = hostPort[(colon + 1)..];
}
return candidate != null && DigitsOnly(candidate) ? candidate : null;
}
/// Decode a raw query string into ordered pairs, preserving duplicates.
private static List<UrlParam> ParseQuery(string? raw) {
var list = new List<UrlParam>();
if (string.IsNullOrEmpty(raw)) return list;
foreach (var pair in raw.Split('&')) {
if (pair.Length == 0) continue; // "a=1&&b=2" yields an empty pair
int eq = pair.IndexOf('=');
list.Add(eq >= 0
? new UrlParam(DecodeParam(pair[..eq]), DecodeParam(pair[(eq + 1)..]))
: new UrlParam(DecodeParam(pair), ""));
}
return list;
}
/// Parse and decompose a URL into a structured report; never throws.
public static UrlReport InspectUrl(string? raw) {
var trimmed = (raw ?? "").Trim();
if (trimmed.Length == 0) return UrlReport.Invalid("URL is empty");
// Absolute + host present mirrors WHATWG validity (relative input fails).
if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var uri) || uri.Host.Length == 0)
return UrlReport.Invalid(InvalidMsg);
var warnings = new List<string>();
string scheme = uri.Scheme.ToLowerInvariant();
string proto = scheme + ":";
// Credentials — UserInfo is the raw "user[:pass]" segment.
string? username = null, password = null;
if (uri.UserInfo.Length > 0) {
int colon = uri.UserInfo.IndexOf(':');
username = colon >= 0 ? uri.UserInfo[..colon] : uri.UserInfo;
password = colon >= 0 ? uri.UserInfo[(colon + 1)..] : null;
}
if (!string.IsNullOrEmpty(username)) warnings.Add("URL contains a username credential");
if (!string.IsNullOrEmpty(password)) warnings.Add("URL contains a password credential");
// uri.Host keeps '[' ']' around IPv6 literals, like WHATWG url.hostname.
string hostname = uri.Host;
string? port = RawPort(trimmed);
string? expected = DefaultPorts.GetValueOrDefault(proto);
bool? defaultPort = port != null ? port == expected : null;
if (defaultPort == true) warnings.Add($"Port {port} is the default for {proto}");
// host drops a scheme-default port (WHATWG serialisation).
string host = port != null && port != expected ? $"{hostname}:{port}" : hostname;
// Query / Fragment carry their leading '?' / '#' already.
string pathname = uri.AbsolutePath.Length > 0 ? uri.AbsolutePath : "/";
var parameters = ParseQuery(uri.Query.Length > 1 ? uri.Query[1..] : null);
if (pathname == "/" && uri.Query.Length < 2 && parameters.Count == 0)
warnings.Add("URL points to the site root (no path or query)");
return new UrlReport {
Valid = true, Protocol = proto,
Username = username, Password = password,
Host = host, Hostname = hostname, Port = port,
Pathname = pathname,
Search = uri.Query.Length > 1 ? uri.Query : null,
Hash = uri.Fragment.Length > 0 ? uri.Fragment : null,
SearchParams = parameters,
Origin = OriginSchemes.Contains(scheme) ? $"{scheme}://{host}" : null,
IsSecure = scheme is "https" or "wss",
DefaultPort = defaultPort, Warnings = warnings,
};
}
public static void Main() {
var r = InspectUrl(
"https://user:pass@example.com:8443/docs/api?q=hello+world&tags=a&tags=b&path=%2Fhome#section");
Console.WriteLine($"protocol {r.Protocol} secure={r.IsSecure}");
Console.WriteLine($"creds {r.Username}:{r.Password}");
Console.WriteLine($"host {r.Host} (port {r.Port}, default={r.DefaultPort})");
Console.WriteLine($"path {r.Pathname} search {r.Search} hash {r.Hash}");
Console.WriteLine("params " + string.Join(", ", r.SearchParams.Select(p => $"{p.Key}={p.Value}")));
Console.WriteLine($"origin {r.Origin}");
Console.WriteLine("warnings " + (r.Warnings.Count == 0 ? "(none)" : string.Join(" | ", r.Warnings)));
var d = InspectUrl("http://example.com:80/");
Console.WriteLine($"\nhttp://example.com:80/ -> {string.Join(" | ", d.Warnings)}");
var e = InspectUrl("not a url");
Console.WriteLine($"'not a url' -> valid={e.Valid} ({e.Warnings[0]})");
}
}
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 →