Skip to main content

URL Encoder

Percent-encode special characters so query strings and path segments work correctly in URLs.

Switch to Decoder

Percent-encode a value so it survives being placed inside a URL. The mechanics are simple — unsafe characters become a percent sign plus two hex digits — but choosing the right scope and the right function is where most encoding bugs originate. This page covers what genuinely needs escaping and how context changes the answer.

Only four characters never need escaping

RFC 3986 defines an unreserved set that is safe everywhere in a URI: the ASCII letters, the digits, and exactly four punctuation marks — hyphen, period, underscore, and tilde (-._~). Every other character is either reserved for structure or outside the permitted range, and whether it needs escaping depends on where you are putting it.

That is the useful mental model. Rather than memorising a list of "special" characters, assume anything outside A–Z a–z 0–9 -._~ needs escaping unless you are deliberately using it as a delimiter. Encoding a character that did not strictly need it is harmless; the URL still resolves identically. Failing to encode one that did is what breaks parsing.

Encode components, never whole URLs

The most damaging mistake is running an entire URL through a component encoder. encodeURIComponent("https://example.com/a?b=c") returns https%3A%2F%2Fexample.com%2Fa%3Fb%3Dc — the scheme separator, path separators, and query delimiter have all been escaped, and the result is no longer a URL at all. It is now a single opaque string.

JavaScript provides two functions for this reason. encodeURI is for a complete URL and deliberately leaves the reserved delimiters : / ? # [ ] @ ! $ & ' ( ) * + , ; = intact. encodeURIComponent is for one piece — a single query value, one path segment — and escapes those delimiters because inside a component they are data, not structure.

The rule that follows: build the URL from encoded pieces rather than encoding an assembled URL. Encode each parameter value with the component function, then join with ?, &, and = yourself. The URL builder does this assembly if you would rather not hand-roll it.

Where encodeURIComponent falls short of the spec

encodeURIComponent predates RFC 3986 and does not escape six characters the spec classes as reserved sub-delimiters: !, ', (, ), *, and it also leaves ~ alone in older engines. For ordinary web use this is fine. It stops being fine when a signature is involved.

AWS Signature V4, OAuth 1.0a, and most webhook HMAC schemes canonicalise the request by percent-encoding parameters to a strict definition before hashing. If your client escapes * and the server does not, the two canonical strings differ by one character and the signature check fails with an unhelpful error. The standard workaround is to post-process:

encodeURIComponent(v).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())

Uppercase hex digits matter here too. %2F and %2f decode identically, but canonicalisation rules generally mandate uppercase, and a lowercase pair will break a signature just as surely as a missing escape.

Space is the character with two right answers

A space encodes as %20 under percent-encoding and as + under application/x-www-form-urlencoded. Both are correct in their own context, and both appear constantly in query strings, which is why this causes so much confusion.

%20 is valid everywhere — in paths, in query strings, in fragments. + is only meaningful as a space inside form-encoded data. If you are unsure which context you are in, use %20: a server that expects form encoding will still decode it correctly, whereas a path segment containing + will be read as a literal plus.

The trap runs the other way as well. If the value you are encoding genuinely contains a plus — a signed number, a timezone offset like +05:30, a phone number in E.164 form — it must become %2B. Leave it bare in a query string and a form-decoding server turns your plus into a space.

Non-ASCII text is encoded as UTF-8 bytes

Percent-encoding operates on bytes, not characters, so any non-ASCII character is first serialised to UTF-8 and then each byte is escaped separately. é becomes %C3%A9, the euro sign becomes %E2%82%AC, and an emoji occupies four escapes. One character routinely produces two, three, or four percent sequences, which is why encoded international text looks so much longer than the original.

A single-byte escape for an accented character — %E9 for é — signals Latin-1 encoding from a legacy system. Modern decoders will reject it as invalid UTF-8 or replace it with �. Encode to UTF-8 first if you are bridging an older stack.

Finally, check whether your value is already encoded before running it through again. Encoding %20 a second time yields %2520, and that mistake tends to surface far downstream, in a log or a redirect target, long after the point where it was introduced.

ThenCatch is a free developer toolkit. Learn more about us.