URL Encoding the Ampersand: %26 in Query Strings, Nested URLs and HTML

In a query string the ampersand is structure, not text: it separates one parameter from the next. So a literal `&` inside a value has to be written as `%26`, otherwise everything after it becomes a new, broken parameter. HTML adds a second, unrelated rule: inside an `href` the `&` should be written `&`. Mixing up those two layers causes most of the ampersand bugs that reach production, and both are easy to get right once you see where each one applies.

What an unescaped & does to a URL

RFC 3986 lists & among the reserved "sub-delims" characters, and the application/x-www-form-urlencoded format used for query strings splits on it. A search for "R&B" sent as ?q=R&B is therefore read as two parameters, q=R and an empty B:

new URLSearchParams("q=R&B").get("q");     // 'R'
[...new URLSearchParams("q=R&B")];         // [ [ 'q', 'R' ], [ 'B', '' ] ]

new URLSearchParams("q=R%26B").get("q");   // 'R&B'

No error is raised anywhere. The server just receives a truncated value, which is why these bugs survive testing with simple inputs and appear only with real data such as company names, song titles or search phrases.

encodeURIComponent versus encodeURI

JavaScript has two encoders, and only one of them escapes the ampersand. encodeURI is designed for a complete URL, so it leaves every character that has a structural meaning, & included, untouched. encodeURIComponent is for a single value and escapes it:

encodeURIComponent("Tom & Jerry");               // 'Tom%20%26%20Jerry'
encodeURI("https://x.test/?q=Tom & Jerry");      // 'https://x.test/?q=Tom%20&%20Jerry'  (& left as is)

The rule of thumb: encode each value on its own with encodeURIComponent, then join the pieces. Running encodeURI over a string that already contains user values cannot know which ampersands are separators and which are data, so it keeps all of them as separators.

Even simpler, let the URL API build the query. URLSearchParams escapes & as %26 and spaces as +, and it never confuses a value with a separator:

const u = new URL("https://api.test/search");
u.searchParams.set("q", "R&B & soul");
u.searchParams.set("json", JSON.stringify({ a: "x&y" }));
u.href;
// 'https://api.test/search?q=R%26B+%26+soul&json=%7B%22a%22%3A%22x%26y%22%7D'

Passing a URL that contains & as a parameter

Share links, redirect targets and "return to" parameters carry an entire URL as a value. If that inner URL has its own query string, its ampersands silently break the outer one. A playlist link is a typical case:

const inner = "https://music.test/watch?v=abc&list=PL123";

"https://share.test/?url=" + inner;
// outer parser sees url=https://music.test/watch?v=abc  and  list=PL123

"https://share.test/?url=" + encodeURIComponent(inner);
// 'https://share.test/?url=https%3A%2F%2Fmusic.test%2Fwatch%3Fv%3Dabc%26list%3DPL123'

With the unencoded version, new URL(...).searchParams.get("url") returns https://music.test/watch?v=abc and the playlist is lost. Encoding the whole inner URL as one component turns its ?, = and & into %3F, %3D and %26, so it arrives intact and the receiving side decodes it once.

JSON in a query parameter follows the same rule: serialise it, then encode the result as a single value. Do not hand-escape only the ampersands; quotes, braces and # need escaping too, and encodeURIComponent or URLSearchParams handles all of them.

The same rule in Python and PHP

Server-side libraries behave the same way, with one difference worth knowing in Python: quote() treats / as safe by default, so pass safe="" when encoding a value that may contain a slash. For the ampersand itself, every function below produces %26:

from urllib.parse import quote, urlencode, parse_qs

quote("Tom & Jerry", safe="")      # 'Tom%20%26%20Jerry'
urlencode({"q": "R&B"})            # 'q=R%26B'
parse_qs("q=R&B")                  # {'q': ['R']}  — the unescaped & split the value

// PHP
urlencode("R&B");                         // 'R%26B'
http_build_query(["q" => "R&B", "x" => 1]);  // 'q=R%26B&x=1'

As in JavaScript, build the query from a dictionary or array rather than concatenating strings, and the escaping cannot be forgotten.

Ampersands in HTML: & in href attributes

A URL that is correct for HTTP can still be written incorrectly in HTML. In markup, & starts a character reference, so the separators between parameters should be written & inside an href. The browser decodes & back to & before it uses the URL, so the request is identical.

Why bother, if browsers usually cope with a bare &? Because of legacy named references. The HTML parser recognises some names, such as &copy, &reg and &not, even without a semicolon, and their treatment differs between text and attributes:

<a href="/s?a=1&copy=2&lang=en&reg">…</a>
<!-- the browser reads the href as: /s?a=1&copy=2&lang=en®  -->

<p>/s?a=1&copy=2&lang=en</p>
<!-- text content becomes:          /s?a=1©=2&lang=en      -->

Inside the attribute, &copy= survived because the parser does not decode a legacy reference followed by =. But &reg at the end of the value became ®, and the same URL printed as text turned &copy=2 into ©=2. The HTML standard also defines an "ambiguous ampersand", a bare & followed by letters or digits and a ; that do not form a known name, and treats it as a parse error. Writing &amp; everywhere avoids all of these cases, and template engines that escape attributes, such as Blade, Twig, Jinja and JSX, do it for you.

Double encoding: %2526 and &amp;amp;

If a server receives %2526, the value was percent-encoded twice: & became %26, and then the % became %25. This usually happens when code encodes a value and then passes it to a library that encodes it again. The same mistake in HTML produces &amp;amp;, and in links it produces URLs containing a literal &amp; that the server treats as part of a parameter name.

The fix is always the same: encode exactly once, at the layer that builds the string, and decode exactly once on the receiving side. A URL encoder and decoder is useful to see how many layers of encoding a suspicious value really has; for the codes of other characters, see the URL encoding characters cheat sheet.

Sources and further reading

RFC 3986 defines reserved characters, including &, and percent-encoding. The WHATWG URL Standard defines the application/x-www-form-urlencoded parser and serializer used by URLSearchParams. The WHATWG HTML Standard defines character references, the attribute-value exception for legacy references followed by =, and the ambiguous-ampersand parse error. MDN documents encodeURIComponent() and encodeURI(); the Python urllib.parse and PHP urlencode() and http_build_query() manuals cover the server-side functions.

Frequently asked questions

How do I escape an ampersand in a URL?

Inside a parameter value, write & as %26, for example q=R%26B. Use encodeURIComponent, URLSearchParams, urlencode or an equivalent function rather than replacing characters by hand.

Does encodeURI encode the ampersand?

No. encodeURI is meant for complete URLs and leaves reserved characters such as &, ? and = as they are. Use encodeURIComponent for individual values, which turns & into %26.

How do I pass a URL with & as a query parameter?

Encode the whole inner URL with encodeURIComponent before appending it, so its ? = and & become %3F, %3D and %26. The receiving side decodes it once and gets the original URL back.

Should I use &amp; or & in an href?

Write &amp; in HTML source. The browser converts it back to & before making the request, and it avoids legacy entity names such as &copy or &reg being decoded inside the URL.

What does %2526 mean in a URL?

It is a double-encoded ampersand: & was encoded to %26 and then the % was encoded to %25. Find the code that encodes the value twice and remove one of the steps.

Is & the same as %26 in a URL?

Not in a query string. A bare & separates parameters, while %26 is a literal ampersand inside a value. They are only interchangeable in parts of a URL where & has no special meaning to the server.