URL Encoder Decoder: What is URL Encoding and Why It Matters
The rule behind percent-encoding fits in one sentence. The decisions around it do not. Every real incident comes from one of three questions: should this character be encoded in this position, which encoder should produce the value, and how many times has this same value already been encoded by the time it reaches you. Get those wrong and the symptoms repeat — a parameter that quietly loses half its content, CJK text that arrives as question marks, a route that refuses to match even though the pattern looks correct, and a %2520 sitting in the logs with no obvious author.
This guide follows the order of those decisions instead of restating what percent-encoding is. First, whether to encode at all. Then which API to use. Then the two separate routes that non-ASCII content takes — one for domain names, one for everything after them. Finally, the two failure families worth planning for: double encoding and security payloads built on mismatched decoding. Every encoded value below was produced by running Node, so you can reproduce each one in the URL encoder and decoder.
1. Whether to Encode Depends on Where the Character Sits
RFC 3986 divides URI characters into unreserved, reserved and everything else. That answers what the grammar permits. The more useful engineering question is what you should do and what happens if you do not. The table below is organised by situation, and every row is a decision you will actually face.
| Situation | Correct move | What goes wrong otherwise |
|---|---|---|
A query value contains & or = | Encode as %26 and %3D | The value is cut short and the server parses an extra parameter |
A path segment contains / | Encode as %2F | One filename becomes two directory levels and the route 404s |
A value contains # | Encode as %23 | The rest is treated as a fragment and never sent to the server |
| A value contains a space | %20 or + in a query depending on the consumer; %20 only in a path | The address bar looks fine while the server receives extra or missing characters |
A value contains + | Encode as %2B | It may be turned back into a space, so C++ arrives as C |
You already hold a %XX string | Do not encode it again | It becomes %25XX and one decode downstream yields a literal |
| You hold a complete URL | Use encodeURI, or build it segment by segment | Every structural character is encoded and it is no longer a requestable address |
The last two rows are the ones people skip. Re-encoding a string that is already encoded and encoding an entire address as if it were a value are opposite mistakes: one encodes too much, the other encodes the wrong object. Both share a nasty property — they look correct during local testing and only surface once a real request leaves your machine.
2. Query Values and Path Segments: Two Rule Sets, One API Choice
Two common JavaScript entry points encode "a value", and they do not agree. Measured side by side:
| Raw value | encodeURIComponent | new URLSearchParams |
|---|---|---|
a b | a%20b | q=a+b |
C++ | C%2B%2B | q=C%2B%2B |
中文 教程 | %E4%B8%AD%E6%96%87%20%E6%95%99%E7%A8%8B | q=%E4%B8%AD%E6%96%87+%E6%95%99%E7%A8%8B |
The only divergence is the space. URLSearchParams follows the older application/x-www-form-urlencoded convention and writes a space as +; encodeURIComponent follows RFC 3986 and writes %20. Both are legal. The trouble starts when the consumer does not know which one it is reading.
The plus sign is where the risk concentrates. When parsing, URLSearchParams turns + back into a space and %2B into a real plus. Measured: ?q=a+b%2Bc parses to a b+c. Run it the other way and a front end that concatenates raw user input into the query string sends ?q=C++, which parses back as C followed by two spaces — the user searched for "C++" and the server searched for "C". That is what "the encoder must match the parser" means in practice: if you read with URLSearchParams, write with it too; if anything downstream parses strictly by RFC 3986, keep encodeURIComponent.
Path segments follow a different rule again. A slash in a path is the hierarchy separator, so you must encode each segment separately and then join them rather than calling encodeURIComponent on the whole path. Measured on a path containing a space and CJK text, per-segment encoding gives 2026%20%E6%8A%A5%E5%91%8A/%E7%AC%AC%E4%B8%80%E7%89%88.pdf, with the slash preserved as a separator; encoding the path as a single value gives 2026%20%E6%8A%A5%E5%91%8A%2F%E7%AC%AC%E4%B8%80%E7%89%88.pdf, erasing the boundary between folder and filename. Once a filename legitimately contains a slash, per-segment encoding is the only form that can express it.
3. CJK Text, Emoji and IDNs: Three Different Routes
Non-ASCII content reaches a URL through more than one mechanism. Treating them as one thing leads to wrong conclusions. Each row below is a measured result.
| Content | Mechanism | Measured result |
|---|---|---|
| CJK text in a path or query | Percent-encoding, one escape per UTF-8 byte | 中 becomes %E4%B8%AD |
| Supplementary-plane characters | Same encoding, four bytes per character | Code point U+1F680 becomes %F0%9F%9A%80 |
| A CJK domain name | IDNA to Punycode, labels prefixed xn-- | 中文域名.中国 becomes xn--fiq06l2rdsvs.xn--fiqs8s |
| A German domain name | The same IDNA rules | münchen.de becomes xn--mnchen-3ya.de |
| A Japanese domain name | The same IDNA rules | 例え.テスト becomes xn--r8jz45g.xn--zckzah |
The key distinction: the host goes through Punycode while everything after the host goes through percent-encoding. Hand a complete internationalised URL to a URL parser and the split is obvious. Measured input https://中文域名.中国/路径/报告 2026?q=值 空格 produces:
https://xn--fiq06l2rdsvs.xn--fiqs8s/%E8%B7%AF%E5%BE%84/%E6%8A%A5%E5%91%8A%202026?q=%E5%80%BC%20%E7%A9%BA%E6%A0%BC
The host collapses into two xn-- labels, while the path and query are percent-encoded, with spaces written as %20 and slashes still acting as separators. This is why an internationalised domain works at all in a browser: IDNA conversion happens before the request leaves, and DNS lookups plus the TLS handshake only ever see ASCII. It also explains a common confusion — a certificate listing the host in xn-- form and an address bar showing CJK characters refer to the same site.
4. Double Encoding: Who Added the Second Layer
%2520 means exactly one thing: a % was encoded as %25, turning %20 into %2520. The time goes into finding which layer encoded it one time too many. The clue is that each layer decodes at a different moment:
| Layer | Decoding behaviour | Typical symptom |
|---|---|---|
Browser address bar / fetch | Normalises and escapes structural characters; does not decode the string you passed in | The %25 shown in the address bar is genuinely what was sent |
| Front-end concatenation | Depends on whether an already-encoded value is handed to an encoder again | The most common first extra layer |
| Reverse proxy or gateway | May decode once before forwarding, depending on rewrite rules | Logged parameter values suddenly look "normal" or shorter |
| Framework router | Path parameters are normally already decoded | Decoding again inside the controller breaks them |
| Query-string parser | Decodes each value after splitting on key/value pairs | Decoding again in business code breaks them |
Locate it by working backwards from the raw request line rather than guessing inside the application: capture one real request and record the parameter exactly as it appears on the request line, then compare it with the value the framework injects into your handler. Whichever layer shows one fewer level of percent-escaping is the layer whose upstream added the extra encoding.
Prevention is far cheaper than diagnosis, and it is one rule: raw values travel untouched inside the system and are encoded exactly once, at the outermost boundary. If a component only has access to an already-encoded value — from an old log entry or a third-party callback — decode it back to the raw value first and then encode for the new destination. Decode then encode, never encode on top of encode.
5. Encoding and Security: The Gap Between Check and Use
Encoding is not a security control; it only changes how bytes are written. The real vulnerability appears when the value validation sees is not the value that gets used, with a decode sitting between them. Three payload families exploit that gap:
| Payload | Which encoding gap it exploits | Defence |
|---|---|---|
%2e%2e%2f | An upstream layer decodes and nothing re-validates the result before it becomes a path | Decode to a stable form, then reject any .. segment |
%0d%0a | An unchecked value is concatenated into a response header | Allow only visible ASCII in header values and reject line breaks outright |
%2f%2fevil.example.com or //evil.example.com | Validation checks a string prefix and misses the protocol-relative form | Use an allowlist of redirect targets, never prefix matching |
The measurements show the whole shape of each family: ../etc/passwd encodes to ..%2Fetc%2Fpasswd; decoding %2e%2e%2fetc/passwd once gives back ../etc/passwd; \r\nX-Injected: 1 encodes to %0D%0AX-Injected%3A%201 and decodes straight back into real line breaks; //evil.example.com encodes to %2F%2Fevil.example.com, yet left unencoded it becomes a protocol-relative address that sends the visitor to another site.
Note: percent-escape case carries no meaning — measured, %2f and %2F decode identically — so any string-matching rule that "blocks %2E%2E" fails the moment an attacker changes case or adds a layer. Normalise before validating: decode the value until it stops changing, then inspect the result, instead of pattern-matching the original string.
6. Three Worked Examples
Example 1: a query address built from a value containing a space. The target is this site's encoder page, the search term is annual report and the page is 2. Following the decisions: the search term is a query value, so the space must be encoded. Measured, annual report (13 characters) encodes to annual%20report (15 characters), giving:
https://www.calc-tools.top/en/text/url-encode?q=annual%20report&page=2
Treat that value as though it were a whole address and encode it again and the query becomes q=annual%2520report. Nothing about the link looks broken locally, yet one decode on the server yields a literal string rather than a space. That is the ordinary form of double encoding.
Example 2: searching for C++, and the two fates of a plus sign. This one example demonstrates both rules at once. Encoded correctly, encodeURIComponent('C++') gives C%2B%2B, and new URLSearchParams({q:'C++'}).toString() gives q=C%2B%2B — the two agree. Concatenate the raw input instead and the measured result of ?q=C++ is C plus two spaces, because both plus signs were read as spaces. Going the other way, if the value genuinely needs one space and one plus, such as a b+c, the correct encoding is a+b%2Bc: + stands in for the space and %2B carries the literal plus, and that parses back to a b+c. In a query string a plus sign is a stand-in for a space; a real plus must be written %2B.
Example 3: taking an internationalised URL apart. Normalising https://中文域名.中国/路径/报告 2026?q=值 空格 splits it across two mechanisms. The host 中文域名.中国 becomes xn--fiq06l2rdsvs.xn--fiqs8s through Punycode. The path /路径/报告 2026 becomes /%E8%B7%AF%E5%BE%84/%E6%8A%A5%E5%91%8A%202026, with %20 for the space and the slash still a separator. The query q=值 空格 becomes ?q=%E5%80%BC%20%E7%A9%BA%E6%A0%BC. Three sections, three sets of rules — which is why an internationalised domain name and an internationalised parameter value cannot be handled with the same mental model.
7. Checking Your Assumptions With the Tool
When you are unsure what a value encodes to, measure it instead of recalling it. Three steps:
- Step 1: paste the raw, unencoded value into the URL encoder and decoder, never the whole address. The test for "raw" is whether the string still contains a
%followed by two hexadecimal digits. - Step 2: convert and compare the output with the measured values in the tables above. CJK should cost nine characters per character, a space should be
%20, a literal plus should be%2B. - Step 3: put the tool's output into a real query string and send the request. If the server receives the wrong value, decode in the opposite direction to establish whether something encoded or decoded one time too many.
When the payload is binary or simply long, Base64 is easier to carry: its output uses only letters, digits and + / =, so the only characters you must reason about are those three or four. See Base64 encoding and decoding and where Base64 actually fits in real systems. To audit a whole parameter set at once, scan it with a regular expression tester using %[0-9A-Fa-f]{2} — any remaining unencoded character stands out immediately.
8. Frequently Asked Questions
- If the front end builds parameters with URLSearchParams and the back end parses strictly by RFC 3986, will something break?
- Yes, and the break is concentrated in spaces. URLSearchParams serialises a space inside a value as
+, while a strict RFC 3986 parser treats+as a literal plus, so a value arrives with+where a space used to be. There are only two fixes: agree on one convention across both ends, or switch to encodeURIComponent so spaces are always%20. - An internationalised domain shows CJK characters in the browser. What actually leaves the machine?
- Punycode. IDNA converts
中文域名.中国toxn--fiq06l2rdsvs.xn--fiqs8s, and DNS lookups plus the TLS handshake only ever see that ASCII form. The host uses Punycode while the path and query that follow it still use percent-encoding, so both mechanisms coexist inside a single URL. - A gateway rewrote the URL and the logs now show %2520. How do I find which layer encoded it?
- Read the raw request line rather than the parameters your application prints. Capture one real request and record the parameter exactly as it appears on the request line, then compare it with the value the framework injects into the handler. Whichever layer shows one fewer level of escaping has an upstream that encoded the extra time. Fix it by re-establishing "encode once, at the outermost boundary" rather than adding a compensating decode downstream.
- Why does a path parameter containing a slash fail to match its route?
- Because a slash is the hierarchy separator in a path. Encoding it as
%2Fis grammatically correct, but some servers, gateways and frameworks refuse or pre-decode paths containing%2Ffor safety, so it turns back into a real slash upstream and the route gains an extra level. The more robust answer is to move that kind of value into a query parameter instead of forcing%2Finto a path. - Is there a length limit for URLs, and when should a parameter travel another way?
- There is no single standard. The effective ceiling comes from the browser, the proxies in between and the web server, with typical limits ranging from a few kilobytes to a few tens of kilobytes, and any layer may refuse first. The practical rule: once a query string passes roughly 2,000 characters, stop putting it in the URL and send it in the request body. If it truly must stay in the URL, compress it first or Base64-encode it, remembering that encoding itself inflates the payload by about a third.
9. Three Common Misconceptions
Misconception 1: encoding sanitises input, so encoded means safe. Encoding only changes how bytes are written; it removes nothing. The vulnerability lives in the gap where validation inspects an encoded string while the application later uses a decoded one: pattern-match %2e%2e%2f, decode once more before joining a path, and the protection is gone. Normalise by decoding to a stable form first, then validate the result.
Misconception 2: encoding and decoding are symmetric, so anything encoded can be decoded back. Measured on a pair of obviously matched functions, decodeURIComponent('%2F') returns / while decodeURI('%2F') returns %2F unchanged — they behave differently precisely on reserved characters. Across character sets it is worse: encode with UTF-8 and decode with something else and nothing comes back at all. Symmetry holds only when the rule, the character set and the layer all match.
Misconception 3: letting a downstream layer decode one more time solves the problem. A second decode locks in the consequences of double encoding and conveniently restores deliberately double-encoded payloads such as %252e%252e%252f into working attack strings. Once you have located the extra encoding, remove it at its source instead of compensating with a decode on the consuming side.
10. Three Practical Rules
- Encode in exactly one place. Agree that raw values travel untouched inside the system and are encoded once at the outermost boundary, and add that to your code review checklist. Any encoding call in a middle layer needs a stated reason.
- Test four values before shipping. Send one real request each for a value containing a space, a plus sign, CJK text and an ampersand, and confirm the server receives exactly what was sent. Those four cover the overwhelming majority of encoding failures and cost far less than reading logs afterwards.
- Normalise before validating. Anywhere a URL parameter becomes a path, a filename or a redirect target, decode it to a stable form first and then validate against an allowlist rather than a blocklist of characters. For invisible characters smuggled into values, pair this with tracking down failures caused by invisible characters.
Put those conventions into the code and URL encoding stops being an occasional garbled-text mystery. It becomes one predictable data transformation that happens once, at the exit.