In PHP, how do you put a user's search term into a link's query string, and when do you use rawurlencode(), urlencode() or http_build_query()?
answer
- encode each part, not the whole URL
- rawurlencode: space as %20
- urlencode: space as +
- http_build_query defaults to RFC 1738
- then htmlspecialchars for the href
basics
~20 sEncode each value going into the URL, not the finished URL: rawurlencode() for path segments, http_build_query() or urlencode() for query parameters. Then pass the whole URL through htmlspecialchars() because it sits inside an href attribute.
solid answer
~40 sA link carries two layers of encoding. First, each dynamic **piece** is URL-encoded so it cannot change the URL's structure: `rawurlencode()` follows RFC 3986, writes a space as `%20` and leaves `-_.~` alone, which is right for path segments; `urlencode()` writes a space as `+`, the form-encoding convention for query strings; and `http_build_query(['q' => $term, 'page' => 2])` encodes a whole query from an array, form-style by default (`PHP_QUERY_RFC1738`) or with `%20` if you pass `PHP_QUERY_RFC3986`. Second, the finished URL goes into an HTML attribute, so it passes through `htmlspecialchars()`, which turns the `&` separators into `&`. Never URL-encode the whole URL, which breaks `:` and `/`, and never skip the HTML layer because the parts were URL-encoded.
code
php · 11 lines<?php
declare(strict_types=1);
$term = $_GET['q'] ?? ''; // e.g. "Rome & Florence"
$query = http_build_query(['q' => $term, 'page' => 2]); // q=Rome+%26+Florence&page=2
$url = '/search?' . $query;
$citySlug = rawurlencode('São Paulo'); // S%C3%A3o%20Paulo
?>
<a href="<?= htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">Next page</a>
<a href="/hotels/<?= htmlspecialchars($citySlug, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">Hotels</a>
<p>Results for <?= htmlspecialchars($term, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>go deeper
Recall that values placed in a URL must be URL-encoded, with urlencode() or rawurlencode(), and that the link still goes through htmlspecialchars() in the page.
Explain the + versus %20 difference, which function suits paths versus queries, http_build_query() and its RFC flag, and the order of URL and HTML encoding.
Show how you centralise URL building so encoding cannot be forgotten, and how you tell a URL you build apart from a user-supplied URL that needs validation.
Decide on one URL-building API for the codebase, and a rule that raw string concatenation of URLs with user data does not pass review.
## Two contexts, two encoders A travel site shows "Search again" links that carry the visitor's last search term, such as `/search?q=S%C3%A3o%20Paulo&page=2`. The term passes through two contexts on its way to the browser: 1. **URL context**: inside the query string, characters such as `&`, `=`, `#`, `?` and space have structural meaning. Without encoding, a term like `Rome&page=999` would add a parameter. 2. **HTML attribute context**: the finished URL sits inside `href="..."`, where `&` starts an entity and `"` ends the value. Each context needs its own encoder, applied in order: URL-encode the pieces, assemble the URL, HTML-escape the result. ## The three URL functions | Function | Space becomes | `~` | Typical use | |---|---|---|---| | `rawurlencode()` | `%20` | kept | path segments, any RFC 3986 component | | `urlencode()` | `+` | encoded as `%7E` | a single query-string value, form style | | `http_build_query()` | `+` by default, `%20` with `PHP_QUERY_RFC3986` | depends on mode | a whole query string from an array | All of them leave letters, digits, `-`, `_` and `.` alone and percent-encode everything else as bytes, so a UTF-8 term like `São Paulo` becomes `S%C3%A3o%20Paulo` (or `S%C3%A3o+Paulo`). ## Path segments versus query values - A **path segment**, such as a city slug in `/hotels/{city}`, must use `rawurlencode()`. In a path, `+` is a literal plus sign, so `urlencode()` would turn a space into a real `+`. - A **query value** can use either. Servers, including PHP when it builds `$_GET`, decode `+` as a space in query strings. `http_build_query()` is usually the cleanest choice: it handles several parameters, nested arrays and the separators. `http_build_query()` joins pairs with the `arg_separator.output` ini value (default `&`) unless you pass a separator, and it skips `null` values. ## Do not encode the whole URL `rawurlencode('https://example.com/search?q=Rome')` produces `https%3A%2F%2Fexample.com%2Fsearch%3Fq%3DRome`, which is a single opaque string, not a working link. Encoding is applied to the **values** you insert, never to the separators you wrote. ## The HTML layer still applies After assembly, the URL is text going into an attribute. `htmlspecialchars()` converts `&` to `&` (the browser turns it back before following the link) and would neutralise a stray quote. Skipping this layer usually "works" in testing, because browsers are lenient with bare `&`, but it leaves the attribute context unprotected. The order matters: 1. `http_build_query()` or `rawurlencode()` on the pieces; 2. concatenate the URL; 3. `htmlspecialchars()` on the whole URL when it is echoed. Reversing steps 1 and 3 produces entities inside the query string, such as `%26amp%3B`, that no server will understand. ## When the whole URL comes from the user Encoding protects the structure of a URL *you* build. If the entire URL is user-supplied, such as a reviewer's website, encoding cannot help: a `javascript:` URL contains nothing to encode. Validate it, allow-list `http` and `https`, and only then escape it for the attribute. ## Decoding on the other side The receiving side decodes what the link encoded: - PHP builds `$_GET` by decoding query strings, treating both `+` and `%20` as a space, so either encoder round-trips a search term correctly. - `urldecode()` also turns `+` into a space, while `rawurldecode()` leaves `+` as a plus sign. A value encoded with `urlencode()` and decoded with `rawurldecode()` therefore keeps its `+` characters instead of spaces. - Decoding happens once. Code that calls `urldecode()` on a value from `$_GET` decodes it a second time, which turns an intended `%2B` into a space and is a classic source of "the plus sign disappears" bugs. The general rule mirrors escaping: encode once at the boundary where the context begins, decode once where it ends. ## Showing the term on the page The same search term also appears as text, "Results for São Paulo". That is plain HTML context: `htmlspecialchars($term)`, no URL encoding. One value, three placements, three different encodings: `http_build_query()` for the query, `htmlspecialchars()` for the attribute around it, and `htmlspecialchars()` alone for the visible text.
- Why is urlencode() wrong for a path segment such as a city name?It encodes a space as `+`, which is the form-encoding convention for query strings. In a URL path `+` is a literal plus sign, so `/hotels/New+York` asks for a city called "New+York". `rawurlencode()` writes `%20`, which decodes to a space in any URL component.
- What does http_build_query() do with a null value in the array?It skips the key entirely, so `['q' => 'Rome', 'filter' => null]` becomes `q=Rome`. That is convenient for optional parameters, but means you cannot send an explicitly empty parameter with `null`; use an empty string for that, which produces `filter=`.
saying these in an interview costs you the question
- rawurlencode() the whole URL before putting it in the link
- urlencode() and rawurlencode() produce identical output
- A URL-encoded value needs no HTML escaping inside href
- URL-encoding a user-supplied link makes javascript: URLs harmless
- http_build_query() writes spaces as %20 by default