Home · Blog · URL encoding

URL encoding: why your links have all those %20s

29 Sep 2026 · Data · 6 min read

Copy a link to a file named "my holiday photo.jpg" and you'll often get something like my%20holiday%20photo.jpg. Search for "fish & chips" and the address bar shows fish%20%26%20chips. Those percent signs followed by two hexadecimal digits are percent-encoding (usually called URL encoding), and they exist because URLs have a grammar — and some characters are load-bearing parts of that grammar.

This article explains which characters get encoded and why, the genuinely confusing difference between encoding a full URL and encoding a single value, and the query-versus-path trap that produces links which work right up until they don't.

Why URLs can't just contain anything

A URL is a structured string: the ? starts the query, & separates parameters, # starts the fragment, / separates path segments. If your search term itself contains an & — "fish & chips" — the server can't tell your ampersand from the structural one. Spaces are worse: they're not allowed in URLs at all, and historically they'd get mangled by systems that split text on whitespace.

Percent-encoding solves this by replacing each problematic byte with a % followed by its two-digit hexadecimal value. A space (byte 32, hex 20) becomes %20; an ampersand (byte 38, hex 26) becomes %26. The server decodes them back before use, so "fish & chips" travels safely as fish%20%26%20chips and arrives intact.

Which characters change

CharacterEncodedWhy it's special
space%20Not allowed in URLs at all
&%26Separates query parameters
=%3DSeparates parameter names from values
?%3FStarts the query string
#%23Starts the fragment
/%2FSeparates path segments
%%25The escape character itself
é (non-ASCII)%C3%A9UTF-8 bytes, each encoded

Note the last row: non-ASCII characters are first converted to UTF-8 bytes, and each byte is encoded. The single character "é" becomes two encoded bytes, %C3%A9. An emoji, at four UTF-8 bytes, becomes four encoded triplets — which is why emoji in URLs look so spectacularly long.

The query-vs-path trap

Here is the detail that breaks links. In a query string, a space may be written as + — a leftover from HTML form encoding. So ?q=fish+chips means "fish chips". But in the path part of a URL, + is a literal plus sign. A file actually named a+b.jpg must be encoded as a%2Bb.jpg; write a+b.jpg in a query and the server reads "a b".

Encoding for the wrong position gives you a link that works — right up until it meets a server stricter than the one you tested against. When in doubt, use %20 for spaces everywhere; it's correct in both positions.

Encoding a value vs encoding a whole URL

There are two different operations, and mixing them up is the most common bug:

  • Encoding a component (one value, like a search term): encode everything structural, because the value must not be mistaken for URL grammar. "fish & chips" → fish%20%26%20chips. In JavaScript this is encodeURIComponent.
  • Encoding a full URL: encode only the characters that are illegal, and leave the grammar alone — the :, /, ? and & must survive, or the URL stops being a URL. In JavaScript this is encodeURI.

Run a full URL through a component encoder and you get https%3A%2F%2Fexample.com%2F — technically encoded, practically useless as a link. Run a search term through a full-URL encoder and the & survives, splitting your one parameter into two at the server.

Decoding pitfalls

Decoding has its own traps. A lone % not followed by two hex digits is malformed — decoders should reject it rather than guess. %2 (truncated) and %zz (not hex) are both errors, not data. And double-encoding is a quiet epidemic: encoding an already-encoded string turns %20 into %2520 (the % itself gets encoded to %25), which decodes to the literal text "%20" instead of a space. If your URLs are growing percent signs like barnacles, something is encoding twice.

Forms: where the + comes from

The +-for-space convention isn't part of URLs proper — it comes from HTML forms. When you submit a form with method="get", the browser serialises your inputs as application/x-www-form-urlencoded: spaces become +, and other special characters get percent-encoded. That's why search URLs so often show pluses instead of %20. Servers know to decode form data with the plus rule, but only for form data — apply the same decoding to a URL path and you'll corrupt any genuine plus signs. This split personality is ancient, well-documented, and still the source of a steady stream of bugs.

International domain names: punycode

What about non-ASCII characters in the domain itself, like münchen.de? The domain name system is ASCII-only, so internationalised domains use a separate encoding called punycode, which compresses Unicode into an ASCII string prefixed with xn--: münchen.de becomes xn--mnchen-3ya.de. Your browser shows you the pretty Unicode version and sends the punycode version on the wire. It's a different encoding from percent-encoding, solving a different problem (domain names rather than paths), and the two can appear in the same URL without interfering.

Fragments never leave the browser

One more structural fact worth knowing: everything after the # — the fragment — is never sent to the server at all. It's purely for the browser: scroll to this section, open this tab, restore this app state. Single-page applications stuff significant data into fragments precisely because it stays client-side. Encoding still applies if the fragment contains special characters, but no server will ever decode it — which means fragment-handling bugs are always yours to find in your own JavaScript, never in server logs.

Worked example: building a search URL by hand

Let's assemble a real URL for searching "café & croissant" on a fictional shop, to see every rule fire at once. The search term contains three things that need encoding: the "é" (non-ASCII), the space, and the ampersand.

  1. Encode the value as a component. "é" becomes its UTF-8 bytes C3 A9, each encoded: %C3%A9. The space becomes %20 (or + in a form context). The ampersand becomes %26, so it isn't mistaken for a parameter separator. Result: caf%C3%A9%20%26%20croissant.
  2. Slot it into the URL grammar. https://shop.example/search?q=caf%C3%A9%20%26%20croissant&lang=en — note the structural & before lang stays bare, while the data ampersand inside the value stays encoded. That contrast is the whole game: grammar bare, data encoded.
  3. Check the decode. The server splits parameters at the bare &, takes q's value, percent-decodes it back to "café & croissant", and searches. If we'd forgotten to encode the data ampersand, the server would have seen three parameters — q=caf%C3%A9%20, %20 (nameless), and %20croissant — and the search would silently return wrong results.

Silent wrongness is the signature failure of encoding bugs: nothing errors, the page loads, and the data is just quietly wrong. When a search or a form behaves oddly, check the address bar — the encoded values usually confess immediately.

The honest caveat

Percent-encoding makes a URL valid; it says nothing about whether the URL is safe or trustworthy. An encoded character is still the same character after decoding — encoding "evil" as %65vil doesn't sanitise it, and servers must validate decoded input like any other. Encoding is plumbing, not security. Treat it as what it is — the reason your links survive the journey intact — and validate everything that comes out the other end.

Try the URL Encoder