March Docs

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

typeURIURI = URI(String, String, Option(Int), String, String, String)#

Functions

fndecodedecode(s)#

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"
fndecode_querydecode_query(s)#

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("")
[]
fnencodeencode(s)#

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"
fnencode_queryencode_query(pairs)#

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([])
""
fnfragmentfragment(uri)#

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"))
""
fnhosthost(uri)#

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"
fnmergemerge(base, relative)#

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/"
fnmerge_strictmerge_strict(base, relative)#

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")
fnparseparse(s)#

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.

fnpathpath(uri)#

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"))
""
fnportport(uri)#

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/"))
None
fnqueryquery(uri)#

Extract 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"))
""
fnschemescheme(uri)#

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"))
""
fnto_stringto_string(uri)#

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"