Uri
URI module: URL/URI parsing, encoding, and query-string manipulation.
Types: URI(scheme, host, port, path, query, fragment) scheme -- e.g. "https", "http", "" if absent host -- e.g. "example.com" port -- Some(443) or None path -- e.g. "/api/users" (includes leading /) query -- raw query string e.g. "q=hello&page=2" fragment -- fragment identifier e.g. "section-1"
Percent-encoding follows RFC 3986: unreserved chars (A-Z a-z 0-9 - _ . ~) are left as-is; all other bytes are encoded as %XX (uppercase hex).
encode_query / decode_query work with List((String, String)) association lists (not Map) so this module has no stdlib dependencies.
Types
Functions
Percent-decode a URI component.
%XX sequences are decoded to their byte values; + is left unchanged (use decode_query for form-encoded values where + means space).
march> Uri.decode("hello%20world")
"hello world"
march> Uri.decode("caf%C3%A9") == "café" -- the REPL prints non-ASCII bytes escaped
true
march> Uri.decode("no-encoding")
"no-encoding"Decode a URL query string into a list of key-value pairs.
Percent-encoded sequences are decoded; "+" is treated as a space (application/x-www-form-urlencoded convention).
march> Uri.decode_query("q=hello+world&page=2")
[("q", "hello world"), ("page", "2")]
march> Uri.decode_query("")
[]Percent-encode a string for safe use in a URI component.
Unreserved characters (A–Z, a–z, 0–9, -, _, ., ~) are left unchanged; all other bytes are encoded as %XX (uppercase hex).
march> Uri.encode("hello world")
"hello%20world"
march> Uri.encode("foo=bar&baz")
"foo%3Dbar%26baz"
march> Uri.encode("café")
"caf%C3%A9"Encode a list of key-value pairs as a URL query string.
Keys and values are percent-encoded; pairs are joined with "&".
march> Uri.encode_query([("q", "hello world"), ("page", "2")])
"q=hello%20world&page=2"
march> Uri.encode_query([])
""Extract the fragment identifier, without the #; "" when absent.
Examples
march> Uri.fragment(Uri.parse("/doc?x=1#section-1"))
"section-1"
march> Uri.fragment(Uri.parse("/doc"))
""Extract the host component, e.g. "example.com"; "" when the URI has no authority. An authority whose port is not an integer is kept whole as the host.
Examples
march> Uri.host(Uri.parse("https://example.com:8080/a"))
"example.com"
march> Uri.host(Uri.parse("a/b"))
""
march> Uri.host(Uri.parse("http://h:abc/x"))
"h:abc"Resolve a relative URI reference against a base URI per RFC 3986 §5.2.
- If the relative URI has a scheme, it is used as-is (absolute
reference), with its dot segments removed.- If it has an authority (host), the base scheme is kept and the
rest comes from the relative.- Empty relative path → keep the base path; the query comes from the
relative if it has one, else from the base; the fragment always
comes from the relative.- Absolute relative path (starts with
/) → replace the base path. - Otherwise → merge with the base path's directory (§5.2.3).
In every case with a path, dot segments are removed per §5.2.4 ("Remove Dot Segments"), and an absolute path keeps its leading /.
A .. that would climb above the root is dropped, as the RFC specifies, so ../../../x against /a/b/c resolves to /x, the same as ../../x. Use merge_strict if you need to detect such over-traversal.
march> Uri.to_string(Uri.merge(Uri.parse("https://example.com/a/b/c"), Uri.parse("../d")))
"https://example.com/a/d"
march> Uri.to_string(Uri.merge(Uri.parse("https://example.com/a/b/c"), Uri.parse("//other.com/x")))
"https://other.com/x"
march> Uri.to_string(Uri.merge(Uri.parse("https://example.com/a/b/c"), Uri.parse("/x?y=1")))
"https://example.com/x?y=1"
march> Uri.to_string(Uri.merge(Uri.parse("https://example.com/a/b/c"), Uri.parse("../../../x")))
"https://example.com/x"
march> Uri.to_string(Uri.merge(Uri.parse("https://example.com/a/b/c"), Uri.parse("./")))
"https://example.com/a/b/"Like merge, but returns Err("...") if the resolved path would escape "above" the root of the path it is resolved into — that is, if any .. segment has no segment left to remove. Useful when the relative reference comes from untrusted input and you want to reject directory-traversal attempts rather than silently absorb them.
Against /a/b/c (directory /a/b/), ../../x reaches the root and is allowed; a third .. climbs above it and is rejected:
march> Uri.merge_strict(Uri.parse("https://example.com/a/b/c"), Uri.parse("../d"))
Ok(URI("https", "example.com", None, "/a/d", "", ""))
march> Uri.merge_strict(Uri.parse("https://example.com/a/b/c"), Uri.parse("../../etc/passwd"))
Ok(URI("https", "example.com", None, "/etc/passwd", "", ""))
march> Uri.merge_strict(Uri.parse("https://example.com/a/b/c"), Uri.parse("../../../etc/passwd"))
Err("Uri.merge_strict: relative reference traverses above base")
march> Uri.merge_strict(Uri.parse("https://example.com/a/b/c"), Uri.parse("/../etc/passwd"))
Err("Uri.merge_strict: relative reference traverses above base")Parse a URI string into its components, per RFC 3986 §3 / §4.2.
march> Uri.parse("https://example.com:8080/api/users?q=hello#top")
URI("https", "example.com", Some(8080), "/api/users", "q=hello", "top")
march> Uri.parse("http://localhost/path")
URI("http", "localhost", None, "/path", "", "")
march> Uri.parse("/relative/path?foo=bar")
URI("", "", None, "/relative/path", "foo=bar", "")Relative references (no scheme) are supported. Only // introduces an authority: a network-path reference (//host/path) has a host and port, while a relative-path reference (../d, a/b) and an absolute-path reference (/a/b) have an empty host and port None, and the whole string before any ? or # is the path:
march> Uri.parse("../d")
URI("", "", None, "../d", "", "")
march> Uri.parse("//other.com:81/x")
URI("", "other.com", Some(81), "/x", "", "")
march> Uri.parse("a/b?q=1")
URI("", "", None, "a/b", "q=1", "")A scheme is recognised only before a :// with no /, ? or # ahead of it, so /go?to=http://x is a path with a query.
Extract the path component, including its leading / when absolute; "" for an authority with no path.
Examples
march> Uri.path(Uri.parse("https://example.com/api/users?x=1"))
"/api/users"
march> Uri.path(Uri.parse("https://example.com"))
""Extract the explicit port as Some(n), or None when the authority has none (no default port is inferred from the scheme).
Examples
march> Uri.port(Uri.parse("http://localhost:4000/"))
Some(4000)
march> Uri.port(Uri.parse("https://example.com/"))
NoneExtract the raw (still percent-encoded) query string, without the ?; "" when absent. Use decode_query to split it into pairs.
Examples
march> Uri.query(Uri.parse("/search?q=a%20b&page=2#top"))
"q=a%20b&page=2"
march> Uri.query(Uri.parse("/search"))
""Extract the scheme component, e.g. "https"; "" for a scheme-less reference.
Examples
march> Uri.scheme(Uri.parse("https://example.com/a"))
"https"
march> Uri.scheme(Uri.parse("/relative"))
""Convert a URI back to its string representation.
march> Uri.to_string(URI("https", "example.com", None, "/path", "q=1", "top"))
"https://example.com/path?q=1#top"
march> Uri.to_string(URI("http", "localhost", Some(4000), "/", "", ""))
"http://localhost:4000/"
march> Uri.to_string(Uri.parse("//other.com/x"))
"//other.com/x"