How to parse a URL
Short answer: Url::parse takes a &str and returns Result<Url, url::ParseError>. The Url it hands back carries the pieces as accessors — scheme(), host_str(), path(), query_pairs() — and query_pairs() is already percent-decoded.
The dependencies
Section titled “The dependencies”One crate, no runtime and no async:
[dependencies]url = "2.5.8"cargo new urldemo gives you the project; that line goes under the [dependencies] it already wrote. 2.5.8 is what the resolver picks for url = "2" today, and the API below is the 2.x line.
Parse it and read the parts
Section titled “Parse it and read the parts”use url::Url;
fn main() { let url = Url::parse("https://www.rust-lang.org/learn?edition=2024#getting-started").unwrap(); println!("as_str: {}", url.as_str()); println!("scheme: {}", url.scheme()); println!("host: {:?}", url.host_str()); println!("port: {:?}", url.port()); println!("port_or_known_default: {:?}", url.port_or_known_default()); println!("path: {}", url.path()); println!("query: {:?}", url.query()); println!("fragment: {:?}", url.fragment()); println!(); println!("as_str: {}", mail.as_str()); println!("scheme: {}", mail.scheme()); println!("host: {:?}", mail.host_str()); println!("path: {}", mail.path());}Run it with cargo run (cargo’s own Compiling and Finished lines are not shown here):
as_str: https://www.rust-lang.org/learn?edition=2024#getting-startedscheme: httpshost: Some("www.rust-lang.org")port: Noneport_or_known_default: Some(443)path: /learnquery: Some("edition=2024")fragment: Some("getting-started")
as_str: mailto:[email protected]scheme: mailtohost: Nonepath: [email protected]Three details in that output are worth a second look:
port()isNoneeven though the URL ishttps. It reports the port only when the URL states one explicitly — give the same hosthttps://example.com:8443/xandport()comes backSome(8443).port_or_known_default()fills in 443 for thehttpsscheme, which is usually the number you want.as_str()is the normalised form, and normalising adds a path. Parsehttps://example.comwith no path at all and the result serialises ashttps://example.com/, withpath()reporting/.- A
mailto:URL has no host. For a scheme with no authority component there is nothing to hand back,host_str()isNone, and the address sits inpath(). Code that unwrapshost_str()meets its firstmailto:link and panics.
Read the query string
Section titled “Read the query string”use url::Url;
fn main() { let url = Url::parse("https://example.com/search?q=hello%20world&lang=en-US&tag=rust%2Blang&plus=a+b").unwrap(); for (key, value) in url.query_pairs() { println!("{key} = {value}"); }}q = hello worldlang = en-UStag = rust+langplus = a bquery_pairs() returns an iterator of (Cow<str>, Cow<str>), decoded on the way out: %20 is a space and %2B is a literal +. A literal + in the input goes the other way and decodes to a space, which is the application/x-www-form-urlencoded rule and the subject of How to parse a query string. A value that must carry a real + needs it encoded as %2B, as the tag pair shows.
Resolve a relative link against a base
Section titled “Resolve a relative link against a base”The other half of the job is join, which resolves a relative reference against a base URL:
use url::Url;
fn main() { let base = Url::parse("https://example.com/docs/book/ch04-00.html").unwrap(); for relative in ["ch04-01.html", "../ch05-00.html", "/index.html", "https://other.example/x"] { println!("{relative} -> {}", base.join(relative).unwrap()); }}ch04-01.html -> https://example.com/docs/book/ch04-01.html../ch05-00.html -> https://example.com/docs/ch05-00.html/index.html -> https://example.com/index.htmlhttps://other.example/x -> https://other.example/x.. climbs out of the last path segment, a leading / replaces the path entirely, and an absolute URL comes back unchanged. This is the call a link scraper needs; the same job done by string concatenation is where the bugs live.
Where parsing fails
Section titled “Where parsing fails”Url::parse returns a Result, and the failure is common enough to plan for:
use url::Url;
fn main() { for candidate in ["example.com", "/just/a/path", "https://"] { match Url::parse(candidate) { Ok(url) => println!("{candidate:?} parsed as {url}"), Err(e) => println!("{candidate:?} failed: {e:?}"), } }}"example.com" failed: RelativeUrlWithoutBase"/just/a/path" failed: RelativeUrlWithoutBase"https://" failed: EmptyHostexample.com is not a URL: there is no scheme, so there is nothing to parse, and join is the only way to give it a base. https:// names the scheme and then no host. The blocks above unwrap their parses because the strings are literals known to be valid; a URL read from a file or an argument gets its Result handled instead.
- How to download a file over HTTP — the previous article, where the URL is the thing you already have.
- How to make a GET request — the request itself, from a
Urlyou built. - How to parse a query string — the form-encoding side of
query_pairs().
The next article in this chapter is about the TLS certificate error, and why turning verification off is the wrong fix.