Skip to content

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 →