A space in a URL is percent-encoded as %20 under the general URI rules, while + for a space comes from the HTML form encoding, application/x-www-form-urlencoded, which applies to query strings and form bodies. Both are right in their own place: %20 works anywhere in a URL, + means a space only where the receiver decodes the query as form data. PrismKit, a keyboard-first Windows utility from Automata Labs, includes a URL encoder and decoder that runs offline, so you can check what a string really contains without pasting it into a website.
Encode and decode URLs offline, from a hotkey, without a browser tab.
Get it from Microsoft
Where does %20 come from?
RFC 3986, the URI standard, defines percent-encoding: a byte is written as % followed by two hex digits. Its unreserved set is letters, digits, hyphen, period, underscore and tilde:
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
A space is not in that set, and it is not a reserved delimiter either, so in a URI it must be percent-encoded. Its byte is 0x20, which gives %20. That is valid in a path, a query and a fragment.
Where does + come from?
From HTML forms. The WHATWG URL Standard defines the application/x-www-form-urlencoded format, and its percent-encoding steps include a spaceAsPlus option: when it is set and the byte is 0x20, the output is a + instead. That is the format browsers use for form submissions and that URLSearchParams produces.
Microsoft’s documentation for .NET’s WebUtility.UrlEncode puts the distinction plainly: replacing a space with a plus sign “is expected only in query part of URI (by convention, not by standard) or in application/x-www-form-urlencoded HTTP content.”
So + is a space only by agreement between the sender and a receiver that parses the query as form data. In a path, + is just a plus sign.
What do common functions produce?
We ran these in Node 20 and Python 3.13 on the string a b&c=d/é+?:
| Function | Output | Space as |
|---|---|---|
| JavaScript encodeURIComponent | a%20b%26c%3Dd%2F%C3%A9%2B%3F | %20 |
| JavaScript URLSearchParams | q=a+b%26c%3Dd%2F%C3%A9%2B%3F | + |
| Python urllib.parse.quote | a%20b%26c%3Dd/%C3%A9%2B%3F | %20 (and / left as is) |
| Python urllib.parse.quote_plus | a+b%26c%3Dd%2F%C3%A9%2B%3F | + |
Notes from the official docs:
- encodeURIComponent. MDN’s encodeURIComponent reference lists the characters it leaves alone: ASCII letters and digits plus - _ . ! ~ * ’ ( ). It adds that for application/x-www-form-urlencoded, “spaces are to be replaced by +”, so you may want to follow it with a replacement of %20 by +.
- quote_plus. Python’s urllib.parse docs describe it as “Like quote(), but also replace spaces with plus signs, as required for quoting HTML form values.” Unlike quote, it does not leave / unencoded by default.
- Uri.EscapeDataString. Microsoft’s reference says it converts characters outside the unreserved set to their hex representation, so a space becomes %20.
Both encodings encode a literal + as %2B. That is the detail that saves you.
Check what a query string really decodes to before you ship the fix.
Get it from Microsoft
What bugs does the difference cause?
A plus sign turns into a space. The classic: a phone number “+1 555 0100” or an email tag “[email protected]” is put into a query string without encoding. The receiver decodes it as form data and gets a space. The fix is to encode the value, so + becomes %2B.
A space stays as a +. The reverse: something builds a query with + for spaces, and the receiver decodes it with a plain percent-decoder. In our Node test, decodeURIComponent(“a+b”) returns a+b, while new URLSearchParams(“q=a+b”).get(“q”) returns “a b”. Same input, two answers, depending on the decoder.
Double encoding. A value is encoded twice, so % becomes %25 and %20 arrives as %2520. Look for %25 in logs; it nearly always means double encoding.
Path versus query. A file name with a space in a path must be %20. Using + in a path gives a file name with a literal plus.
What about encodeURI versus encodeURIComponent?
They answer different questions, and mixing them up causes many of the bugs above. encodeURI is for a whole URL: it leaves the characters that give a URL its structure alone. In our Node test, encodeURI(“a b&c=d/?”) returned a%20b&c=d/?, with the space encoded but the ampersand, equals sign, slash and question mark left as they were. That is right for a complete address and wrong for a single value, because an & inside a value would start a new parameter.
encodeURIComponent is for one piece, such as a single query value or path segment. It encodes those structural characters too, so the value cannot break out of its slot.
On the decoding side, Python’s parse_qs and JavaScript’s URLSearchParams are both form-aware. In our tests, each decoded q=a+b and r=a%20b to “a b”, and s=a%2Bb to “a+b”, which is exactly what you want from a query parser.
Which should I use?
| Situation | Use |
|---|---|
| Building a query string for an HTML form or a server that parses form data | Either works for spaces; + is the form convention. Always encode + as %2B |
| A value going into a URL path | %20 |
| You do not know how the receiver decodes | %20, which every percent-decoder reads as a space |
| Decoding a query string | A form-aware parser, such as URLSearchParams or parse_qs |
| Signing a URL, as some APIs require | Exactly what the API’s docs specify; encodings must match byte for byte |
The safest general rule: encode each value with a function made for components, and let your language’s URL or query builder assemble the pieces.
How do I check a string quickly?
When a request fails and you suspect encoding, decode it and look. That is where a local tool helps. In PrismKit, press your hotkey, choose the URL encoder or decoder from the command palette, and paste the string. Because PrismKit runs offline and local-first, a query string carrying a token or customer data never leaves your machine. The same palette has a JWT inspector, a Base64 encoder and decoder and a JSON formatter for the rest of the request. Our guide to Base64, URL and JSON encoding tools covers those side by side.
If a decoded value looks right but the API still rejects it, check the JSON body too. Our explainer on JSON parse unexpected token errors covers the common causes.
Frequently asked questions
Is + in a URL always a space?
No. Only in a query string or form body that the receiver decodes as application/x-www-form-urlencoded. Elsewhere it is a literal plus.
Is %20 ever wrong?
Not as a space in a URL. A form-data decoder reads %20 as a space too.
Why does my browser’s address bar show spaces?
Browsers often display decoded characters for readability. What is sent over the network is encoded.
Is PrismKit free?
The core tools, including the URL encoder and decoder, are free. Optional add-on packs are one-time purchases.
Try PrismKit
PrismKit is free in the Microsoft Store, with optional lifetime add-on packs. Install it, set your hotkey, and decode the next query string you would have pasted into a browser tab. More detail is on the PrismKit product page.
Keep encoders, decoders and formatters one hotkey away, offline.
Get it from Microsoft
Last updated 22 October 2026. Published by Automata Labs, a software company in Canada.