In Ruby, how do URI.parse, URI.join and URI.encode_www_form help build a request URL, and what trap hides in URI.join?
answer
- never interpolate query strings
- URI::InvalidURIError on a raw space
- trailing slash decides the join
- space becomes +, not %20
- URI.escape gone since 3.0
basics
~20 sURI.parse turns a String into a URI::HTTP or URI::HTTPS object, URI.join resolves a relative path against a base, and URI.encode_www_form builds an escaped query. URI.join drops the base's last path segment unless it ends in a slash.
solid answer
~30 s`URI.parse(str)` (or `URI(str)`) returns a `URI::HTTPS` or `URI::HTTP` object with `host`, `port`, `path`, `query` and `request_uri`, and raises `URI::InvalidURIError` for characters such as a raw space. `URI.join(base, ref)` resolves `ref` like a browser resolves a link: `URI.join("https://api.example.com/v1", "forecast")` gives `https://api.example.com/forecast` because the base has no trailing slash, and a `ref` starting with `/` replaces the whole path. `URI.encode_www_form({city: "New York", days: 3})` returns `"city=New+York&days=3"`, escaping each key and value, so you assign it with `uri.query = ...` instead of interpolating. `URI.escape` was removed in Ruby 3.0; use `URI.encode_www_form_component` or `URI.encode_uri_component` for single values.
code
ruby · 10 linesrequire "uri"
base = URI("https://api.example.com/v1/")
uri = URI.join(base, "forecast")
uri.query = URI.encode_www_form({city: "New York", days: 3})
puts uri
# => https://api.example.com/v1/forecast?city=New+York&days=3
puts uri.request_uri
# => /v1/forecast?city=New+York&days=3go deeper
Know that URI.parse gives an object with host, port and path, and that query values must be encoded with URI.encode_www_form.
Explain URI.join's resolution rules, the + versus %20 difference between the component encoders, and request_uri.
Catch the trailing-slash bug in configured base URLs and replace removed URI.escape calls during upgrades before they fail at runtime.
Standardise URL construction in a shared client so no team interpolates parameters, making injection of extra query keys structurally impossible.
## Why build URLs with the uri library Gluing a URL together with string interpolation (`"https://api.example.com/v1/forecast?city=#{city}"`) works until a value contains a space, an ampersand, a `#` or a non-ASCII letter. Then the URL is invalid, or worse, the value leaks into another parameter. Ruby's **`uri`** default gem, loaded by `require "net/http"` and by open-uri, gives you parsing, resolution and escaping. ## URI.parse and URI() **`URI.parse(string)`** returns an object whose class depends on the scheme: `URI::HTTPS` for `https`, `URI::HTTP` for `http`, `URI::Generic` for schemes it does not know. `URI(string)` is a `Kernel` shortcut for the same thing. Readers you use with Net::HTTP: - `host` and `hostname` (the latter strips IPv6 brackets); - `port`, which falls back to 443 or 80 when the string has none; - `path`, `query` and **`request_uri`**, the path plus query that goes on the request line; - `scheme`, which the Net::HTTP helpers use to decide on TLS. Since Ruby 3.4 the default parser follows RFC 3986. A string containing a raw space or another character that URLs do not allow raises **`URI::InvalidURIError`**; escape values before they reach the string, rather than parsing and hoping. ## URI.join and the trailing-slash trap **`URI.join(base, *refs)`** resolves each reference against the base the way a browser resolves a relative link. The rules surprise people: | Base | Reference | Result | |---|---|---| | `https://api.example.com/v1/` | `forecast` | `https://api.example.com/v1/forecast` | | `https://api.example.com/v1` | `forecast` | `https://api.example.com/forecast` | | `https://api.example.com/v1/` | `/forecast` | `https://api.example.com/forecast` | The last segment of a base without a trailing slash is treated as a "file" and replaced, and a reference starting with `/` replaces the whole path. A client configured with a base URL of `https://api.example.com/v1` and paths like `"forecast"` silently calls the wrong endpoint. Two safe habits: store base URLs with a trailing slash and join relative references, or build the path explicitly and set `uri.path`. ## URI.encode_www_form for queries **`URI.encode_www_form(enum)`** takes a Hash or an Array of pairs and returns a query String in `application/x-www-form-urlencoded` form: - each key and value goes through `URI.encode_www_form_component`, which keeps `*`, `-`, `.`, `_` and alphanumerics, turns a space into `+`, and percent-encodes everything else as UTF-8 bytes; - an Array value repeats the key: `{day: [1, 2]}` gives `day=1&day=2`; - a `nil` value emits the bare key. Assign the result with `uri.query = URI.encode_www_form(params)`; `request_uri` then carries it. The reverse is `URI.decode_www_form(query)`, which returns an Array of `[key, value]` pairs. For a single value, choose the component encoder by where it goes: 1. `URI.encode_www_form_component(value)` for a query value (space becomes `+`); 2. `URI.encode_uri_component(value)` for a path segment or any place where `+` would be read literally (space becomes `%20`). ## Building from parts When every component is already separate, skip string assembly entirely. `URI::HTTPS.build(host: "api.example.com", path: "/v1/forecast", query: URI.encode_www_form(city: "Oslo"))` returns a `URI::HTTPS` object; `URI::HTTP.build` does the same for plain http. The builder checks each component, so an empty host or a path without a leading slash is rejected. Going the other way, `URI.decode_www_form(uri.query).to_h` turns an incoming query back into a Hash, which is handy in tests that assert what a client sent. For repeated keys, keep the Array of pairs instead of calling `to_h`, which keeps only the last value per key. ## Removed APIs you will still meet **`URI.escape` and `URI.unescape` were removed in Ruby 3.0.** Older answers and blog posts still use them; on Ruby 4.0 they raise `NoMethodError`. Replace them with the component encoders above, or with `URI::RFC2396_PARSER.escape` when you truly need the old whole-string behaviour. ## Checklist - Parse once, then modify the object (`uri.path`, `uri.query`) rather than editing Strings. - End base URLs with `/` before calling `URI.join` with relative paths. - Never interpolate user-supplied values into a query; encode them.
- What does URI.encode_www_form({tag: ["hot", "dry"], raw: nil}) return?`"tag=hot&tag=dry&raw"`. An Array value repeats its key once per element, and a nil value produces the bare key without `=`, which is how HTML forms and most servers read multi-valued and flag parameters.
- Why does URI("https://api.example.com/v1").request_uri matter to Net::HTTP::Get.new(uri)?When a request object is built from a URI, Net::HTTP uses `request_uri`, the path plus query, as the request target and sets `Host` from the URI's authority. Setting `uri.query` before building the request is therefore how the encoded query reaches the server.
saying these in an interview costs you the question
- URI.join always appends the reference to the base path
- URI.encode_www_form encodes a space as %20
- URI.parse quietly escapes spaces it finds in the string
- URI.escape is the standard way to encode a query value in Ruby 4.0
- String interpolation is fine for query values that come from users