URL Encoding: %20, %2F, Plus Signs & RFC 3986
Two escaping systems, one job each. Percent-encoding makes a value safe to travel inside a URL: %2F is an escaped slash, %3F an escaped question mark, %20 an escaped space. HTML entities solve a different problem entirely, making text safe inside markup rather than inside an address. Which one you need, what RFC 3986 actually requires, and the three mistakes that break most links are below; to convert a real string instead, the free URL Encoder / Decoder applies exactly those rules locally.
Free and local. It applies the encodeURIComponent rule set described above and turns a + back into a space when decoding, so form-encoded input round-trips.
percent-encode or decode a URL value in your browser →The whole thing in thirty seconds
Type hello world & grüße into the URL encoder and you get hello%20world%20%26%20gr%C3%BC%C3%9Fe. Every space became %20, the ampersand became %26 because a raw & would start a new query parameter, and ü became the two triplets %C3%BC because ü is two bytes in UTF-8 and each byte is written as a percent sign plus two hex digits. The pairs worth memorising: space is %20, & is %26, = is %3D, ? is %3F, # is %23, + is %2B, and / is %2F. The é in café is %C3%A9, and an emoji like 😀 costs four bytes, %F0%9F%98%80. Everything below explains why each of those is what it is, and the three mistakes — plus signs, double encoding, and Latin-1 instead of UTF-8 — that break most URLs. None of the rules need working out by hand: the encoder linked from this page applies exactly this table, and the section at the end of this guide explains why it escapes the reserved characters a URL is built from.
What RFC 3986 says must be encoded
RFC 3986 defines a URI as scheme, then a hierarchical part, then an optional query, then an optional fragment. It sorts characters into two named sets. The unreserved set is ALPHA, DIGIT, hyphen, period, underscore and tilde. These carry no structural meaning, so there is no reason to encode them and the RFC says a producer should not. The reserved set is everything that might act as a delimiter: gen-delims is : / ? # [ ] @, and sub-delims is ! $ & ' ( ) * + , ; =. Reserved characters are only encoded when they appear in a component where they are meant as data rather than syntax. The distinction the RFC draws is that a URI and the same URI with a reserved character percent-encoded are not equivalent, which is why substituting %26 for & in a query value changes the meaning. The encoding itself is mechanical: a percent sign, then two uppercase hex digits per byte. So the value foo & bar becomes foo%20%26%20bar, and file/name as a single path segment becomes file%2Fname. An unencoded # is the classic silent failure: the browser treats everything after it as a fragment and the rest never reaches the server.
UTF-8 bytes to percent sequences
Percent-encoding operates on bytes, not characters, and any character outside ASCII must be converted to UTF-8 first. The letter e with an acute accent is one code point but two bytes in UTF-8, C3 and A9, so it encodes as caf%C3%A9. The Japanese string こんにちは is five code points, each three bytes, which is fifteen bytes and therefore fifteen percent triplets. The emoji U+1F600 is four bytes, F0 9F 98 80, so it becomes %F0%9F%98%80 and a single character costs twelve characters in the URL. Two mistakes produce this. The deprecated escape() function emits %uXXXX, a form that never belonged to URI syntax and is not UTF-8. And encoding Latin-1 bytes instead of UTF-8 gives %E9 for the accented letter rather than %C3%A9, which a UTF-8 server decodes as garbage. RFC 3987 defines the IRI, the internationalised form permitting non-ASCII directly, and describes the conversion to a URI as percent-encoding the UTF-8 bytes. The URL encoder here uses encodeURIComponent, which is UTF-8 correct for these cases.
encodeURI versus encodeURIComponent in practice
The two functions differ in exactly one respect: how much they leave alone. encodeURIComponent leaves only letters, digits, and the seven characters - _ . ! ~ * ' ( ). Everything else gets escaped, including the whole reserved set. encodeURI leaves the unreserved set plus the full reserved set, so structure survives. Use encodeURIComponent for values: const url = `https://example.com/search?q=${encodeURIComponent(userInput)}`. Run userInput through encodeURI and an ampersand starts a new parameter; run a full URL through encodeURIComponent and every slash becomes %2F, which most servers will not match. Note that encodeURIComponent still leaves ! ' ( ) and * unescaped even though they are sub-delims under RFC 3986. That is a leftover from the older RFC 2396 mark set, harmless in a query value, but escape them yourself where those characters are significant. The encoder here uses encodeURIComponent and the decoder uses decodeURIComponent, additionally treating + as a space so form-encoded input round-trips.
The plus sign trap
A space can be written %20 or + depending on where you are, and this is the most common source of subtly broken URLs. The plus convention comes from application/x-www-form-urlencoded, the content type browsers submit HTML forms with. In that context a + is a space, which means a literal plus has to be sent as %2B. Query strings produced by URLSearchParams follow the form convention: new URLSearchParams({ q: 'a & b' }).toString() gives q=a+%26+b, and that is what a server parsing the form encoding expects. Outside form parsing a plus is a plus, so a path segment /file+name names a file with a plus in it. The failure is asymmetric: send a plus where %2B was meant and you get a space; send a space where a plus was meant and you get a plus. If you control both ends, always use %20 and never emit a bare plus.
Double encoding and normalisation
Double encoding is the same mistake applied twice. The raw string a b is correct as a%20b. Encoding that output again turns the percent sign into %25, giving a%2520b. A server that decodes once receives the literal string a%20b, including a percent sign that was never meant to be data, and the value does not match. The structural fix is to stop treating encoded text as data: keep the decoded value in your store, and encode exactly once when you construct the URL. Detecting it in an existing URL means decoding and then checking whether the result still contains a percent sign followed by two hex digits. Normalisation has other rules worth knowing. RFC 3986 section 5.2.4 defines remove_dot_segments, and the URL constructor applies it, so https://example.com/a/./b/../c resolves to a path of /a/c. Percent-encoding hex digits should be uppercase, and a decoder must accept either case, so %2f and %2F are the same octet.
Injection and validation order
Encoding is a transport concern, not a security boundary, and the failures are predictable. Reflecting an unencoded value into an HTML attribute or a script context is an injection route, because the attacker controls characters that terminate the context. Encode at the output edge and escape for the context you are writing into, since percent-encoding does not neutralise a quote character. Path traversal depends on validation order: a check that runs before decoding accepts ..%2F, because at that point the string looks like a harmless literal, and the server decodes it afterwards. Validate after decoding, then re-encode when you build the redirect. An open redirect follows the same ordering error, where a target beginning with // is checked while still encoded and becomes a protocol-relative URL once decoded. A malformed escape is worth rejecting, since treating a percent sign not followed by two hex digits as a literal silently corrupts the value.
Copy-paste examples
Encode a query value: encodeURIComponent('price < 100 & shipping = free') gives price%20%3C%20100%20%26%20shipping%20%3D%20free. Decode an emoji: decodeURIComponent('%F0%9F%98%80') returns the character. Build safely with the platform: const u = new URL('https://example.com'); u.searchParams.set('q', 'a & b'); — searchParams serialises with the form convention. In Python, urllib.parse.quote(value, safe='') for a value and unquote to reverse. In Go, url.QueryEscape for a query value and url.PathEscape for a path segment. In PHP, rawurlencode for the %20 style and urlencode for the form-plus style. For a path segment specifically, a safe template is base + '/' + encodeURIComponent(segment), which also encodes a slash that would otherwise change the path structure. To verify any of this by hand, round-trip it: encode the value, decode the result, and confirm you get the original string back exactly. Any mismatch means an extra or a missing step.