2026-07-02 · Calc-Tools Editor

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.

SituationCorrect moveWhat goes wrong otherwise
A query value contains & or =Encode as %26 and %3DThe value is cut short and the server parses an extra parameter
A path segment contains /Encode as %2FOne filename becomes two directory levels and the route 404s
A value contains #Encode as %23The 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 pathThe address bar looks fine while the server receives extra or missing characters
A value contains +Encode as %2BIt may be turned back into a space, so C++ arrives as C
You already hold a %XX stringDo not encode it againIt becomes %25XX and one decode downstream yields a literal
You hold a complete URLUse encodeURI, or build it segment by segmentEvery 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 valueencodeURIComponentnew URLSearchParams
a ba%20bq=a+b
C++C%2B%2Bq=C%2B%2B
中文 教程%E4%B8%AD%E6%96%87%20%E6%95%99%E7%A8%8Bq=%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.

ContentMechanismMeasured result
CJK text in a path or queryPercent-encoding, one escape per UTF-8 byte中 becomes %E4%B8%AD
Supplementary-plane charactersSame encoding, four bytes per characterCode point U+1F680 becomes %F0%9F%9A%80
A CJK domain nameIDNA to Punycode, labels prefixed xn--中文域名.中国 becomes xn--fiq06l2rdsvs.xn--fiqs8s
A German domain nameThe same IDNA rulesmünchen.de becomes xn--mnchen-3ya.de
A Japanese domain nameThe 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:

LayerDecoding behaviourTypical symptom
Browser address bar / fetchNormalises and escapes structural characters; does not decode the string you passed inThe %25 shown in the address bar is genuinely what was sent
Front-end concatenationDepends on whether an already-encoded value is handed to an encoder againThe most common first extra layer
Reverse proxy or gatewayMay decode once before forwarding, depending on rewrite rulesLogged parameter values suddenly look "normal" or shorter
Framework routerPath parameters are normally already decodedDecoding again inside the controller breaks them
Query-string parserDecodes each value after splitting on key/value pairsDecoding 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:

PayloadWhich encoding gap it exploitsDefence
%2e%2e%2fAn upstream layer decodes and nothing re-validates the result before it becomes a pathDecode to a stable form, then reject any .. segment
%0d%0aAn unchecked value is concatenated into a response headerAllow only visible ASCII in header values and reject line breaks outright
%2f%2fevil.example.com or //evil.example.comValidation checks a string prefix and misses the protocol-relative formUse 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:

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 中文域名.中国 to xn--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 %2F is grammatically correct, but some servers, gateways and frameworks refuse or pre-decode paths containing %2F for 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 %2F into 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

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.

Online URL Encode/Decode

Free to use, no download required

Use URL Encode/Decode Now

Tools