URL encoding, and the two functions people confuse
Why a space becomes %20, when encodeURI is wrong and encodeURIComponent is right, and the double-encoding bug that follows from getting it backwards.
Last reviewed · 1,373 words
In short
- Percent encoding replaces unsafe characters with a % and their hexadecimal byte value. A space becomes %20.
- encodeURIComponent is what you want almost always. encodeURI leaves reserved characters alone and is only for whole URLs.
- Encoding an already-encoded string produces %2520 instead of %20, and the resulting URL silently points somewhere else.
- The plus sign means a space in query strings and a literal plus in paths. This inconsistency is historical and permanent.
- Non-ASCII characters are encoded as UTF-8 bytes, so a Devanagari character becomes three percent-escapes.
A URL can only contain a limited set of characters. Everything else has to be encoded, and the mechanism is percent encoding: replace the character with a % followed by the hexadecimal value of each byte.
A space is byte 0x20, so it becomes %20.
What has to be encoded, and why
Three groups of characters cause trouble.
Reserved characters have a structural meaning in a URL: : / ? # [ ] @ ! $ & ' ( ) * + , ; =. A ? starts the query string, a # starts the fragment, an & separates parameters. When one of these appears as data rather than as structure, it must be encoded or it will be read as structure.
Unsafe characters — space, quotes, angle brackets, braces, backslash, caret, backtick, pipe — cause problems in transmission, in email clients that break long URLs across lines, and in software that has to guess where a URL ends.
Non-ASCII characters cannot appear in a URL at all. They are converted to UTF-8 bytes first, and each byte is percent-encoded.
Unreserved characters — letters, digits, - . _ ~ — never need encoding and should never be encoded.
The two JavaScript functions
This is the distinction that causes most real bugs.
encodeURIComponent encodes everything except unreserved characters. Use it for a piece of a URL: a query parameter value, a path segment, a fragment.
encodeURI leaves the reserved characters alone, because it assumes you are handing it a complete URL whose structure should survive.
The difference in practice:
| Input | encodeURIComponent | encodeURI |
|---|---|---|
hello world | hello%20world | hello%20world |
a/b | a%2Fb | a/b |
a?b=c | a%3Fb%3Dc | a?b=c |
a&b | a%26b | a&b |
Consider building a search URL for the query cats & dogs:
?q= + encodeURIComponent(cats & dogs) gives ?q=cats%20%26%20dogs — correct, one parameter.
?q= + encodeURI(cats & dogs) gives ?q=cats%20&%20dogs — the & survives as structure, so the server sees a parameter q with the value cats and a second parameter named with no value.
The rule: use encodeURIComponent for values, encodeURI for whole URLs, and in practice you almost always want the first.
Double encoding
Encoding a string that is already encoded is the second most common bug, and it is quiet.
%20 encoded again becomes %2520, because the % itself gets encoded to %25. Decoding once gives %20 back — a literal percent-two-zero as text, not a space.
It happens when a value is encoded in one layer of an application and encoded again in another, which is easy when a URL is built in one place and passed through a helper somewhere else that assumes raw input.
Symptoms: %2520 visible in the address bar, spaces appearing as literal %20 in page content, a redirect landing on a URL that looks nearly right.
The fix is to establish exactly one place responsible for encoding and to pass raw values everywhere else. Decoding twice to "fix" it is a workaround that breaks on any value containing a genuine %.
Plus signs and spaces
A space can be %20 or +, and which one applies depends on where in the URL it is.
In a query string, + means a space. This comes from the application/x-www-form-urlencoded format used by HTML forms since the beginning of the web.
In a path, + is a literal plus sign.
So /search?q=a+b searches for "a b", and /files/a+b.txt refers to a file with a plus in its name.
The consequence: a literal plus in a query value must be encoded as %2B, or it will be read as a space. This is the reason phone numbers in URLs lose their leading plus, and email addresses using the user+tag@example.com convention break in password reset links that were not built carefully.
%20 is valid everywhere and is the safer choice when building a URL yourself.
Non-ASCII and internationalised domains
A character outside ASCII is encoded as its UTF-8 bytes, each percent-escaped. The Devanagari character न is three bytes in UTF-8, so it becomes %E0%A4%A8 — three escapes for one character.
This is why a URL containing an Indian-language phrase looks enormous when encoded, and it is correct.
Domain names work differently. They cannot use percent encoding, so internationalised domains use Punycode: भारत.com becomes xn--h2brj9c.com. The xn-- prefix marks it.
Browsers display the readable form and transmit the Punycode. The security concern is the homograph attack — Cyrillic "а" looks identical to Latin "a", so a lookalike domain can be registered. Browsers now display Punycode rather than the readable form when a domain mixes scripts suspiciously.
Where encoding matters
Query parameters with user input. Search terms, filter values, anything typed by a person. Always encodeURIComponent.
Path segments containing data. A product name or a filename in a URL path needs encoding, particularly for slashes and question marks.
Redirect URLs passed as parameters. ?next=https://example.com/a?b=c needs the whole target encoded, or the outer parser will stop at the inner ?.
Anything constructed by string concatenation. The safest approach in modern JavaScript is the URL and URLSearchParams objects, which handle encoding correctly without you deciding — params.set('q', value) encodes the value properly whatever it contains.
Analytics and tracking parameters, which frequently carry URLs inside URLs and are a reliable source of double-encoding bugs.
Decoding, and where it goes wrong
Decoding is mechanically simpler and has two failure modes worth knowing.
A malformed sequence. A % not followed by two hexadecimal digits is invalid,
and decodeURIComponent throws a URIError rather than returning something
approximate. Any code decoding user-supplied input needs to handle that, because a
crafted URL will otherwise produce a server error.
Invalid UTF-8. Percent escapes decode to bytes, and those bytes must form valid UTF-8. A sequence that does not — frequently from text encoded in a legacy codepage such as ISO-8859-1 and then percent-escaped — throws for the same reason.
The second is why old links sometimes cannot be decoded at all. A URL built in 1998 with Latin-1 bytes is not valid UTF-8 today, and there is no general way to recover the intended characters without knowing the original encoding.
Query strings, and the parser you should use
Building a query string by concatenating pieces is where most encoding bugs enter a codebase.
The modern answer in JavaScript is URLSearchParams, which encodes each value
correctly without any decision from you and handles repeated keys, empty values
and ordering consistently. new URL() does the same for whole URLs, including
resolving a relative path against a base.
Server frameworks have equivalents, and every one of them is more reliable than string concatenation.
Two details worth knowing about query parsing. Repeated keys — ?a=1&a=2 —
have no standard behaviour: some parsers keep the first, some the last, and some
produce an array. And an empty value — ?a= — is distinguishable from an
absent key by most parsers and not by all, which occasionally matters for a filter
that means "match empty" rather than "no filter".
Encoding in other contexts
Percent encoding is one of several escaping schemes, and using the wrong one for the context is its own class of bug.
HTML entity encoding protects characters that have meaning in markup:
&, <, >, ". A URL placed inside an HTML attribute needs
both — percent encoding for the URL structure and entity encoding for the
attribute — which is why an ampersand in a link href is correctly written
& in the HTML source and still means a single & in the URL.
JavaScript string escaping uses backslashes and is different again.
SQL escaping is different again, and the correct answer there is not to escape at all but to use parameterised queries.
The general principle: escape at the boundary, for the destination, and only once. A value that will be placed in a URL inside an HTML attribute rendered by a template engine passes three boundaries, and each needs its own encoding applied by whatever handles that layer. Doing it manually at the wrong point is how both double-encoding and injection vulnerabilities arise.
What this tool assumes
- Encoding uses percent encoding with UTF-8 for non-ASCII characters, matching current standards.
- The component mode encodes reserved characters; the full-URL mode preserves them.
- Decoding a malformed sequence — a
%not followed by two hexadecimal digits — is reported rather than guessed at. - Plus signs are treated as literal on encode; decode mode offers both interpretations, because the correct one depends on context.
- Everything runs in your browser. Nothing you paste is sent anywhere.