toolhq.io

All posts
6 min readby Jameel Haider

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 valueWrong (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 repeated tag keeps only the last value.
  • Node with the qs library and Ruby on Rails accept both the bracket form and plain repeated keys as arrays.
  • Express with the default querystring collects repeated plain keys into an array automatically.
  • Python urllib.parse.parse_qs returns 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.