URL Inspector — Kotlin 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 Kotlin 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. Kotlin (1.9+) port of src/lib/url-inspector.ts — same logic as this dir's java.java (java.net.URI underneath, so explicit default ports stay visible); the WHATWG touches ('/'-for-empty-path, default-port stripping from host) are re-imposed by hand.
import java.net.URI
import java.net.URISyntaxException
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
/** A single decoded query parameter, in insertion order (duplicates preserved). */
data class UrlParam(val key: String, val value: String)
/** Flat decomposition; null mirrors the TS undefined-when-absent fields. */
data class UrlReport(
val valid: Boolean,
val warnings: List<String> = emptyList(),
val protocol: String? = null,
val username: String? = null,
val password: String? = null,
val host: String? = null,
val hostname: String? = null,
val port: String? = null,
val pathname: String? = null,
val search: String? = null,
val hash: String? = null,
val searchParams: List<UrlParam> = emptyList(),
val origin: String? = null,
val isSecure: Boolean? = null,
val defaultPort: Boolean? = null,
) {
companion object {
fun invalid(warning: String) = UrlReport(valid = false, warnings = listOf(warning))
}
}
object UrlInspector {
/** Well-known default ports per scheme, keyed WHATWG-style with the ':'. */
private val DEFAULT_PORTS = mapOf("http:" to "80", "https:" to "443", "ftp:" to "21", "ws:" to "80", "wss:" to "443")
/** Schemes that yield a non-opaque origin (others serialise origin as "null"). */
private val ORIGIN_SCHEMES = setOf("http", "https", "ws", "wss", "ftp")
private const val INVALID = "Invalid URL - could not be parsed (include the scheme, e.g. https://)"
/**
* Percent-decode a query value; URLDecoder also converts '+' to a space —
* exactly the query-string semantic. Malformed escapes fall back to the
* original, like the TS try/catch.
*/
fun decodeParam(v: String): String = try {
URLDecoder.decode(v, StandardCharsets.UTF_8)
} catch (e: IllegalArgumentException) {
v
}
/** Decode a raw query string into ordered pairs, preserving duplicates. */
fun parseQuery(raw: String?): List<UrlParam> {
if (raw.isNullOrEmpty()) return emptyList()
return raw.split('&').filter { it.isNotEmpty() }.map { pair ->
val eq = pair.indexOf('=')
if (eq >= 0) UrlParam(decodeParam(pair.take(eq)), decodeParam(pair.substring(eq + 1)))
else UrlParam(decodeParam(pair), "")
}
}
/** Parse and decompose a URL into a structured report; never throws. */
fun inspectUrl(raw: String?): UrlReport {
val trimmed = raw?.trim().orEmpty()
if (trimmed.isEmpty()) return UrlReport.invalid("URL is empty")
val uri = try {
URI(trimmed)
} catch (e: URISyntaxException) {
return UrlReport.invalid(INVALID)
}
// WHATWG validity: an absolute scheme AND a server-based authority are
// both required (getHost() is null for registry-based / scheme-less URIs).
val scheme = uri.scheme?.lowercase() ?: return UrlReport.invalid(INVALID)
val hostname = uri.host?.takeIf { it.isNotEmpty() } ?: return UrlReport.invalid(INVALID)
val proto = "$scheme:"
val warnings = mutableListOf<String>()
// Credentials — getRawUserInfo() is "user[:pass]", raw (still encoded).
val info = uri.rawUserInfo
val username = info?.substringBefore(':')?.takeIf { info.contains(':') || it.isNotEmpty() }
val password = if (info != null && info.contains(':')) info.substringAfter(':') else null
if (!username.isNullOrEmpty()) warnings += "URL contains a username credential"
if (!password.isNullOrEmpty()) warnings += "URL contains a password credential"
// getPort() is -1 when no port was written (defaults are NOT injected),
// and getHost() keeps IPv6 brackets, like WHATWG url.hostname.
val p = uri.port
val port = if (p >= 0) p.toString() else null
val expected = DEFAULT_PORTS[proto]
val defaultPort = port?.let { it == expected }
if (defaultPort == true) warnings += "Port $port is the default for $proto"
// host drops a scheme-default port (WHATWG serialisation).
val host = if (port != null && port != expected) "$hostname:$port" else hostname
// WHATWG serialises an empty hierarchical path as "/".
val pathname = uri.rawPath.ifEmpty { "/" }
val params = parseQuery(uri.rawQuery)
if (pathname == "/" && uri.rawQuery.isNullOrEmpty() && params.isEmpty())
warnings += "URL points to the site root (no path or query)"
val origin = if (scheme in ORIGIN_SCHEMES) "$scheme://$host" else null
return UrlReport(
valid = true,
protocol = proto,
username = username?.takeIf { it.isNotEmpty() },
password = password?.takeIf { it.isNotEmpty() },
host = host,
hostname = hostname,
port = port,
pathname = pathname,
search = uri.rawQuery?.takeIf { it.isNotEmpty() }?.let { "?$it" },
hash = uri.rawFragment?.takeIf { it.isNotEmpty() }?.let { "#$it" },
searchParams = params,
origin = origin,
isSecure = scheme == "https" || scheme == "wss",
defaultPort = defaultPort,
warnings = warnings,
)
}
}
fun main() {
val r = UrlInspector.inspectUrl(
"https://user:pass@example.com:8443/docs/api?q=hello+world&tags=a&tags=b&path=%2Fhome#section")
println("protocol ${r.protocol} secure=${r.isSecure}")
println("creds ${r.username}:${r.password}")
println("host ${r.host} (port ${r.port}, default=${r.defaultPort})")
println("path ${r.pathname} search ${r.search} hash ${r.hash}")
println("params ${r.searchParams.joinToString(", ") { "${it.key}=${it.value}" }}")
println("origin ${r.origin}")
println("warnings ${r.warnings.ifEmpty { listOf("(none)") }.joinToString(" | ")}")
val d = UrlInspector.inspectUrl("http://example.com:80/")
println("\nhttp://example.com:80/ -> ${d.warnings.joinToString(" | ")}")
val e = UrlInspector.inspectUrl("not a url")
println("'not a url' -> valid=${e.valid} (${e.warnings.first()})")
}
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 →