Percent-encoding exists because a URL is a structured address, not free text. Characters such as ?, &, =, and / carry structural meaning, so any value that contains them must be escaped before it becomes part of an address. Skip that step and a single ampersand in a search term silently splits one parameter into two.

Most URL bugs come from applying the right function in the wrong place. Encoding a complete URL destroys its structure; encoding nothing corrupts its data. This guide covers the distinction, the functions that implement it, and the failure modes that follow from getting it wrong.

How percent-encoding works

The rule is mechanical. A character that cannot appear literally is converted to its bytes, and each byte is written as a percent sign followed by two hexadecimal digits. A space becomes %20, an ampersand becomes %26, and a question mark becomes %3F.

Because the conversion operates on bytes rather than characters, the character set matters. Modern systems encode text as UTF-8 first, so a character outside ASCII produces several percent sequences. The euro sign becomes %E2%82%AC, which is three bytes rendered as three escapes, not one.

Reserved, unreserved, and everything else

RFC 3986 divides characters into two groups that decide the behaviour. Unreserved characters are letters, digits, and the four symbols -, ., _, and ~. They never require encoding and should never be encoded, because %2D and - are equivalent and the escaped form only makes the URL harder to read.

Reserved characters are the delimiters that give a URL its shape. They are legal in the address but change its meaning, so they must be encoded whenever they appear inside a value rather than as structure. Everything outside both groups, including spaces and non-ASCII text, always requires encoding.

Encoding a component, not a whole URL

This is the decision that matters most. When you have one value destined for a query parameter or a path segment, encode it as a component so that delimiters are escaped. In JavaScript that is encodeURIComponent(), which converts &, =, ?, and / into escapes.

When you have a complete, already-structured URL and only want to make stray characters safe, use encodeURI(), which deliberately leaves delimiters alone. Applying encodeURIComponent() to a full URL turns https://example.com/a?b=c into an unusable string where the scheme separator and query marker have been escaped.

Building query strings correctly

Encode each key and each value separately, then join them with = and &. Encoding the assembled string afterwards escapes the separators you just added and produces a single parameter whose value contains the rest of the query.

In practice, prefer a builder over manual concatenation. Browsers and most server frameworks provide one, such as URLSearchParams, which encodes each pair as it is added and removes an entire class of mistakes. The same applies when reading: parse with the platform parser instead of splitting on & by hand.

Double encoding and how to spot it

Double encoding happens when an already-encoded string is encoded again. The percent sign is itself a character that requires escaping, so %20 becomes %2520. The signature is a URL full of %25 sequences, and the symptom is a parameter value that arrives containing literal escape text such as hello%20world.

The cause is nearly always a layered pipeline where a framework encodes a value that application code had already encoded. Fix it by deciding which layer owns encoding rather than by adding a decode step to compensate, because a value that legitimately contains a percent sign will then be corrupted instead.

Form bodies are a different encoding

The application/x-www-form-urlencoded media type looks like a query string but follows an older convention: spaces become + rather than %20. A decoder that does not know which format it is reading will turn a genuine plus sign in a password or a search term into a space.

Keep the two apart deliberately. Use the form decoder for request bodies submitted by HTML forms, and the standard percent decoder for path segments and query strings. When a value must survive both, encoding the plus sign explicitly as %2B removes the ambiguity.

Decoding safely

Decoding is where security problems appear, because validation performed before decoding checks the wrong string. A path traversal sequence hidden as %2e%2e%2f passes a naive filter and becomes ../ afterwards. The rule is to decode exactly once, at a known boundary, and to validate the decoded result.

Malformed input matters too. A truncated escape such as %2, or one with non-hexadecimal digits, will make a strict decoder throw, and a lenient one guess. Handle the error rather than falling back to the raw string, and be careful about pasting URLs that contain session tokens into any decoder that transmits input to a server.

URL encode and decode in each language

Every platform ships both halves. What differs is which characters the component encoder leaves alone, so a value that round-trips in one language can still break in another.

EnvironmentEncode a componentDecode
URL encode js / URL encoder JavaScriptencodeURIComponent(v)decodeURIComponent(v)
URL encode Pythonurllib.parse.quote(v, safe="")urllib.parse.unquote(v)
URL encoder C# / URL decoder C#Uri.EscapeDataString(v)Uri.UnescapeDataString(v)
URL encoder JavaURLEncoder.encode(v, UTF_8)URLDecoder.decode(v, UTF_8)
PHPrawurlencode($v)rawurldecode($v)
URL encoding bash / URL encode clijq -rn --arg v "$v" '$v|@uri'printf '%b' "${v//%/\\x}"
Percent encoding Rustutf8_percent_encode(v, NON_ALPHANUMERIC)percent_decode_str(v)

To URL encode a string in any of these, pass the single value rather than the assembled address; a URL encode string call on a full URL destroys its structure. A URL decode bash one-liner and a URL encode command line invocation are both in the table, and both are worth wrapping in a small script so the quoting is written once.

Java's URLEncoder is the outlier worth remembering: it implements form encoding rather than pure percent encoding, so it emits + for a space. Using it to build a path segment produces a value that a strict decoder will read incorrectly. For a URL decode command line one-off, a short Python invocation is more predictable than shell parameter expansion, which mishandles multi-byte sequences.

A checklist that prevents most URL bugs

Encode values, never structure. Build query strings with a parser-backed helper rather than string concatenation. Decide which layer of the stack owns encoding and let every other layer pass the value through untouched. Decode once, at a defined boundary, and validate afterwards rather than before.

When a link misbehaves, read it before theorising. Run it through a URL encode decode tool and compare what arrived against what should have been sent: a stray %25 points to double encoding, a value that ends early points to an unescaped delimiter, and a parameter that vanished entirely usually means the whole URL was encoded as a component somewhere upstream.