Query string encoding gotchas
Query strings look simple, but the encoding rules around them are full of edge cases that silently corrupt data. A space might become + or %20 depending on context, a literal plus sign can vanish, and a stray ampersand in a value can split one parameter into two. Knowing where each rule applies keeps your parameters intact end to end.
Space: plus or percent twenty
There are two encodings for a space in a URL, and both are correct in their own context.
- In the path of a URL, a space is
%20. The+character is just a literal plus there. - In a query string encoded as
application/x-www-form-urlencoded, a space is+. This is the format HTML forms submit by default.
So /my%20file?q=hello+world is a path with a space and a query value hello world. Both parts are valid at the same time. The confusion arises because encodeURIComponent always produces %20, never +, even for query values. That is fine: %20 is also accepted in a query string by every compliant parser, so encodeURIComponent output is safe everywhere. The reverse is not true, which leads straight to the next gotcha.
The literal plus sign bug
Because + means space in a form-encoded query string, a real plus sign in a value must be encoded as %2B. If you skip that, the value is quietly altered.
value: c++
naive URL: ?lang=c++
server decodes: c (two spaces) <-- wrong
correct: ?lang=c%2B%2B
server decodes: c++
This bites people with phone numbers in E.164 form (+14155550100), programming language names, and math expressions. The leading + becomes a space and the number loses its country prefix. Always run user values through encodeURIComponent, which encodes + to %2B for you, rather than concatenating raw strings.
Ampersand and equals inside values
The query string uses & to separate parameters and = to split each key from its value. When those characters appear inside a value, they must be encoded or the parser miscounts your parameters.
| Raw value | Wrong (unencoded) | Right (encoded) |
|---|---|---|
a&b | ?x=a&b becomes two params | ?x=a%26b |
a=b | ?x=a=b ambiguous split | ?x=a%3Db |
100% | ?x=100% invalid percent | ?x=100%25 |
The last row is its own trap: a lone % not followed by two hex digits is malformed, and many parsers throw rather than pass it through. Encode % as %25 whenever it is a literal percent sign.
Repeated keys and arrays
There is no official standard for sending a list in a query string, so conventions vary, and the server library decides how to read them.
?tag=red&tag=blue&tag=green
?tag[]=red&tag[]=blue
?tags=red,blue,green
The first, repeated keys, is the most portable. How it is parsed depends on the backend:
- PHP requires the
tag[]bracket form to build an array; plain repeatedtagkeeps only the last value. - Node with the
qslibrary and Ruby on Rails accept both the bracket form and plain repeated keys as arrays. - Express with the default
querystringcollects repeated plain keys into an array automatically. - Python
urllib.parse.parse_qsreturns every key as a list, so repeats come through as multiple elements.
The practical rule is to agree on one convention between client and server and test it, because a value that round-trips in one framework is dropped in another. When in doubt, repeated plain keys are understood most widely, and comma-joined values are simple but break if a value legitimately contains a comma.
To see exactly how a value transforms, encode a string with spaces, plus signs, ampersands, and percent signs, then decode it back. The URL Encoder / Decoder shows both directions so you can confirm your parameters survive the round trip.