August 9, 2026
URL encoding, explained: percent-encoding, reserved characters, and component vs full URLs
You have seen them everywhere: %20 where a space should be, %E4%B8%AD where a Chinese character should be, a wall of %3A%2F%2F at the start of a query value that is itself a URL. Percent-escapes look like noise, but every one of them is a small negotiation between two facts: a URL is a single line of ASCII text, and the data we want to put inside it is anything but.
This guide explains what URL encoding actually is, which characters the URL grammar reserves for itself, the difference between encoding one component and encoding a whole URL (the distinction behind a whole family of bugs), and how base64url relates to all of it. Every example can be reproduced in the URL encode/decode tool on this site, which runs entirely in your browser.
Why percent-encoding exists at all#
A URL has a job that makes it unusual among string formats: it must be transmissible through channels that were never designed for it. A URL gets copied into emails, spoken over the phone, typed into terminals, stuffed into HTTP headers, and painted onto signs. Along the way it passes through systems that treat some bytes specially — control characters, spaces, non-ASCII bytes, #, ?, % itself.
So the URL grammar (standardized in RFC 3986, which unified the older RFC 1738 and RFC 2732 rules) makes a strict demand: a URL is a sequence of ASCII characters, and only a subset of them may appear literally. Characters outside that safe set must travel in a universal escape form:
% XX
a percent sign followed by exactly two hexadecimal digits, naming one byte from 0 to 255. %20 is the byte 0x20 (a space). %3F is the byte 0x3F, which is ?. Two characters are so fundamental they always need escaping when meant literally: % itself becomes %25 (otherwise every % would start an escape), and a space becomes %20 — spaces are legally significant in too many contexts (they terminate tokens in headers, they get trimmed by editors) to survive raw.
For non-ASCII text, the rule since RFC 3986 is UTF-8: the character is serialized to its UTF-8 byte sequence, and each byte is independently percent-encoded. 中 is the three bytes E4 B8 AD, so it becomes %E4%B8%AD. The word 中文 becomes %E4%B8%AD%E6%96%87. This is why one character can expand to nine ASCII characters in a URL — a fact that matters for length limits.
Reserved characters: the grammar’s private property#
RFC 3986 divides characters into two camps, and the distinction explains almost every encoding decision you will make.
Unreserved characters may appear anywhere, in any component, without escaping and without changing meaning: the ASCII letters A-Z a-z, the digits 0-9, and four punctuation marks - . _ ~. A percent-escape of an unreserved character (writing %41 for A) is technically legal but pointless — decoders normalize it away.
Reserved characters are the delimiter set the URL grammar uses to build structure: : / ? # [ ] @ ! $ & ' ( ) * + , ; =. These characters belong to the URL’s syntax. They may still appear literally — but only in the syntactic role. The moment one of them appears as data — as part of a value you are transporting — it must be escaped, because otherwise a reader of the URL cannot tell the delimiter from the payload.
The canonical demonstration: transporting one URL inside the query string of another, the next= pattern behind every login redirect. Take:
https://example.com/redirect?next=/settings
The value of next is a path, /settings. If you insert this URL naively as a query value, the / and ? and = inside it collide with the outer URL’s own delimiters. Encoded as a component, every reserved character is neutralized:
https%3A%2F%2Fexample.com%2Fredirect%3Fnext%3D%2Fsettings
Decoding it back yields the original URL byte for byte. That round-trip property — encode as data, decode as data — is the whole trick.
Component versus full URL: the distinction that matters#
Two functions live in every language’s standard library, and choosing the wrong one is the most common URL bug in application code. JavaScript exposes them plainly: encodeURIComponent and encodeURI. The URL tool offers exactly this pair through its Component / Full selector.
Component encoding escapes everything that is not unreserved. In JavaScript the exact keep-set is A-Z a-z 0-9 - _ . ! ~ * ' ( ) — note it is slightly wider than RFC 3986’s unreserved set, a historical artifact of the spec it was written against. Everything else — /, ?, &, =, #, spaces, all non-ASCII — becomes percent-escapes. Use it on a value you are about to place into a URL: one query parameter, one path segment, one fragment.
Encoding the fragment path/to file?name=值:
path%2Fto%20file%3Fname%3D%E5%80%BC
The / became %2F, the space %20, the ? %3F, the = %3D, and 值 its three UTF-8 bytes as %E5%80%BC. As data, this is now inert: no URL parser will find structure in it.
Full-URL encoding escapes the same unsafe bytes but deliberately leaves the reserved delimiters alone. Feed it:
https://example.com/搜索?q=查询 词
and you get:
https://example.com/%E6%90%9C%E7%B4%A2?q=%E6%9F%A5%E8%AF%A2%20%E8%AF%8D
The protocol colon, the slashes, the ?, the = survive; the Chinese characters and the stray space are escaped. Use it when you already have a complete URL with the right shape and only need to clean up literal characters within it.
The decision rule fits in one sentence: if the character is a delimiter of the URL’s structure, keep it (Full); if the character is part of the data being carried, escape it (Component). When in doubt, encode components and build the URL from pieces — that is the safe default, because component encoding of each value can never break the skeleton.
A related practical rule: encode before assembling, never after. Build ?q= + encodeURIComponent(value) + &lang= + encodeURIComponent(other). Encoding the finished string is how & becomes %26 and your server silently receives one giant parameter instead of three.
Worked examples in real data#
Example 1 — a search query with mixed content. A user searches for 咖啡 & 茶 <list> (coffee & tea, plus angle brackets from a pasted snippet). As a component:
%E5%92%96%E5%95%A1%20%26%20%E8%8C%B6%20%3Clist%3E
Each CJK character contributes three escapes, the & becomes %26 so it cannot spawn a phantom second parameter, and < > become %3C %3E so the value survives being placed inside HTML attributes unmangled.
Example 2 — an email address as an identifier. [email protected] encodes as user%2Btag%40example.com. The + matters more than it looks: in application/x-www-form-urlencoded data (classic HTML form posts, and the q=a+b style of query string), + is defined to mean a space. If you send the raw +, some servers read the address as user [email protected]. Encoding it as %2B is the only way the plus arrives intact — and decoding %2B reliably yields a+b, not a b.
Example 3 — a literal percent sign. The string 50% off must become 50%25 off encoded: %25 is the escape for %. This is the exact spot where double-encoding bugs hatch. If some layer encodes twice, 50% first becomes 50%25, then the % in %25 is itself escaped, producing 50%2525 — and after one decode you see 50%25, after two you see 50%. When a URL shows %25 in the address bar and your page renders a literal %, you are looking at one extra encoding layer; when the page shows %20 as visible text, some component decoded once too few.
Example 4 — malformed escapes. Decoding must refuse garbage. % must be followed by two hex digits, so a%GGb is not a valid encoding, and 100% (a lone trailing percent) is not either — both throw URI malformed in JavaScript, and the URL tool reports the offending position instead of guessing. This strictness is deliberate: a decoder that “does its best” with invalid escapes produces outputs that can never be re-encoded to the same string, breaking the round-trip that percent-encoding exists to guarantee.
Where base64url fits in#
Base64 often appears in URL contexts and is frequently confused with percent-encoding, so it is worth placing precisely. They solve different problems:
- Percent-encoding is about the URL grammar: it makes arbitrary bytes safe inside a URL, at the cost of one third again to three times the length (each byte becomes up to three characters).
- Base64 is about binary-to-text: it represents arbitrary bytes using 64 printable characters, at a fixed cost of 4 characters per 3 bytes (about 133%). It has nothing to do with URL structure by birth.
Standard base64 uses A-Z a-z 0-9 + / plus = padding — and two of those characters are URL-reserved. Put a standard base64 string into a URL and the + may be read as a space, the / may split your path, the = may terminate your query value. The fix is the base64url variant defined for exactly this purpose: + becomes -, / becomes _, and trailing = padding is dropped. A single byte A is QQ== in standard base64 and simply QQ in base64url.
You have already used base64url today if you have decoded a JWT: its three dot-separated segments are base64url-encoded. The header {"alg":"HS256","typ":"JWT"} becomes eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 — no +, /, or = in sight, which is precisely why the format chose this variant. You can verify with the JWT decoder, and round-trip arbitrary text with the base64 tool, which includes a URL-safe mode.
The layered relationship, then: base64url chooses an alphabet that is URL-safe so that it usually needs no percent-encoding at all. Percent-encoding remains the mechanism of last resort for anything that still contains reserved or non-ASCII characters.
A checklist that prevents most real bugs#
- Encode each query value with Component encoding, then join with
&and=. Never encode the assembled string. - Use Full encoding only for repair: you have a complete URL that contains stray spaces or non-ASCII characters.
- A
+in data must travel as%2B; a space may travel as%20(always safe) and as+only inside form-encoded bodies. %in data always travels as%25.- Non-ASCII text is UTF-8 first, then percent-encoded per byte; a 3-byte CJK character becomes 9 characters — budget for length limits.
- When a URL arrives partly encoded, decode fully before re-encoding, or you will double-encode whatever was already correct.
- Debug by decoding each layer once and reading the intermediate result — that is how you find where an extra
%25or a missing%2Bentered.
And one boundary worth respecting: percent-encoding makes text safe for URLs. Placing that same text into HTML requires a different escaping — & as &, < as < — which is the job of the HTML entity tool. URL-encoding does not protect you in an HTML context, and HTML-escaping does not protect you in a URL; mixing up the two is how links break and XSS slips in.
FAQ#
Why does a space sometimes appear as + and sometimes as %20?#
Two specs, one collision. RFC 3986, which governs URLs in general, defines exactly one escape for space: %20. The older application/x-www-form-urlencoded format, used when HTML forms submit POST bodies, defines + as shorthand for space. Both appear in query strings in practice. %20 is correct everywhere; + is only safe where the form-encoding convention is in force.
Is it valid to leave unreserved characters encoded, like %7E for ~?#
Technically yes — decoders must accept it — but it signals an encoder from before RFC 3986 (the old escape function encoded ~; encodeURIComponent does not). Normalizing %7E back to ~ is safe; treating the two forms as different strings is not.
Why did my non-ASCII text become much longer after encoding?#
Each non-ASCII character is first serialized as UTF-8 (1 to 4 bytes), and each byte becomes three characters. A CJK character is typically 3 bytes, so 9 characters in the URL. This is normal, but it is why URLs with embedded Chinese, Arabic, or emoji can blow past length limits that seemed generous.
Component or Full — my URL has both a query and a value that is itself a URL. What do I do?#
Encode the inner URL with Component mode, then place it into the outer URL as a value. The outer URL needs no further encoding if its own literal characters are already ASCII and unreserved. The URL tool makes this a two-step paste: encode the inner value, drop the result into the outer query, verify with Full mode that the assembled URL has no remaining raw spaces or non-ASCII.
Can I decode a string that has a % followed by non-hex characters?#
No, and beware tools that try. %GG and a trailing 100% are malformed; the correct behavior is to fail with a position, exactly as JavaScript’s decodeURIComponent throws URI malformed. Lenient decoding produces strings that cannot round-trip and masks upstream bugs.
How do base64 and percent-encoding interact?#
Independently, in one direction: percent-encoding can encode anything, including a base64 string that contains + or /. Base64url exists so standard base64 payloads usually need no percent-encoding at all. If you control both ends, prefer base64url for binary data in URLs — it is shorter than percent-encoding raw bytes in nearly all cases.
Where to go next#
- Paste your own values into the URL encode/decode tool and watch the Component/Full distinction in both directions.
- Round-trip binary or UTF-8 text, including the URL-safe alphabet, with the base64 converter.
- Escape text destined for HTML with the HTML entity encoder.
- See base64url in the wild by inspecting a token with the JWT decoder.