URL Encode Query Strings — Plus Signs vs %20 Wars

(Updated: July 16, 2026 ) webdev url-encode http query-string tools

Local search for hello world works. After a redirect through a marketing site, the API receives hello+world or hello%2520world and returns zero hits. Spaces in query strings are where URL encoding stops being theoretical and starts looking like “search is flaky.”

Build queries from parts, not from whole URLs

const base = "https://api.example.com/search";
const q = encodeURIComponent("hello world");
const url = `${base}?q=${q}`;
// https://api.example.com/search?q=hello%20world

Wrong mental model:

encodeURIComponent("https://api.example.com/search?q=hello world");
// https%3A%2F%2F...  → useless as a navigation target

Encode values (and sometimes keys). Leave ?, &, =, and scheme punctuation as literal structure. If you need to pass a full URL as a parameter (OAuth redirect_uri, next=), encode that parameter’s value — do not encode the outer URL you are about to fetch.

Plus vs %20 — context decides, not vibes

Historically, form bodies (application/x-www-form-urlencoded) turn spaces into +. Many query parsers accept the same rule for the query string. Path segments do not treat + as space — there + is a plus character.

ContextSpace encoding+ means
Form body+ or %20Usually space
Query (form-style parsers)often eitherOften space
Path%20 (not +)Plus character

When debugging, decode and compare the logical string first. Do not argue about wire form until you know which parser the server uses (URLSearchParams, a framework binder, legacy CGI).

Framework gotchas that look like product bugs

const p = new URLSearchParams({ q: "hello world" });
p.toString(); // q=hello+world

URLSearchParams speaks form-urlencoded. That is valid for many servers. If a CDN signature or partner API expects %20 only, normalize before signing — and put that rule in a single helper so every hop does not invent its own.

Also watch keys: an unencoded & or = inside a value splits parameters. # starts the fragment unless encoded. Non-ASCII must be UTF-8 then percent-encoded. A literal % in user input becomes %25, or a later decode invents sequences you never intended.

Double-encoding on redirect chains

A common production chain:

  1. App encodes once → %20
  2. Open-redirect or analytics middleware encodes the whole next URL again → %2520
  3. Final service decodes once → still sees %20 as literal characters in the search term

Fix by decoding in a loop until the string stabilizes (cap iterations), then encode once for the next hop. Log both wire and decoded forms in staging. Add fixtures for space, plus, ampersand, Japanese text, and an already-encoded %20.

Use a URL encode/decode tool to inspect a suspicious value in isolation — paste the query value, not the entire URL, when you want encodeURIComponent behavior.

Signing, caches, and proxies

HMAC and CDN cache keys often hash the exact query string. Mixing + and %20 for the same logical search produces different signatures and cache misses. Pick one canonical form at the edge that signs, and reject or rewrite the other before the hash.

Reverse proxies that “helpfully” decode and re-encode can mutate order and encoding. If only traffic through one gateway breaks, compare raw request lines before blaming the app.

Practical rules that survive review

  1. Never encodeURIComponent a full absolute URL you intend to navigate.
  2. Prefer %20 when hand-building API URLs unless tests prove + is required.
  3. Treat URLSearchParams output as form-urlencoded (pluses are normal).
  4. After each redirect hop, assert the decoded search term equals the original.
  5. Document unit tests for space, +, &, #, %, and multibyte text.

Query encoding bugs look like flaky search while the wire string quietly mutates at each hop. Encode per component, decode before re-encode, and keep plus-versus-percent debates tied to the actual parser you ship — not to whichever Stack Overflow answer was open that morning.

Signing and caching interact with encoding

CDN cache keys and HMAC signatures see the wire string, not your decoded mental model. If one hop emits + and another normalizes to %20 before signing, signatures fail intermittently. Pick a canonical encoding for signed URLs, normalize once at the signer, and document it. Add a regression test that round-trips spaces, plus signs, and non-ASCII through your redirect chain — encoding bugs love multi-hop marketing links.