Skip to main content

How to URL Encode and Decode Query Parameters Safely: A Developer's Field Guide

Learn the difference between encodeURIComponent and encodeURI, when + vs %20 breaks your API, and how to handle edge cases like nested objects and special characters.

Published
#url-encoding #query-string #web-development #javascript #api

How to URL Encode and Decode Query Parameters Safely

URL encoding sounds simple until you hit your first %2B that was supposed to be a + sign, or a search query that silently drops a user's accented name. I've debugged both in production. This guide covers what actually goes wrong and how to handle it correctly the first time.

Why "Just Encode It" Isn't Enough

RFC 3986 defines two categories of characters in a URL: reserved (:, /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =) and unreserved (A–Z, a–z, 0–9, -, _, ., ~). Everything else must be percent-encoded as %XX where XX is the hexadecimal byte value.

The catch: reserved characters have structural meaning in a URL. If you encode too little, the parser misinterprets your data. If you encode too much, you break the URL structure. JavaScript ships two different functions precisely because of this tension:

  • encodeURI(url) — encodes everything except characters that are legal anywhere in a URL. Leaves :, /, ?, &, =, # untouched. Use this on a complete URL you want to keep intact.
  • encodeURIComponent(value) — encodes everything except unreserved characters. Encodes &, =, +, #, and all other reserved characters. Use this on individual query parameter keys and values.

A concrete example. I once watched a colleague encode an entire URL with encodeURIComponent before appending it to a redirect parameter:

Input:  https://example.com/search?q=café&lang=en
encodeURIComponent result:
https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Dcaf%C3%A9%26lang%3Den

That output is correct if the whole URL is a value being passed as a query parameter. But if it was meant to be an actual navigable URL, every slash and colon got destroyed. The fix is to encode only the value parts, never the full URL:

Input value:  café & tea
encodeURIComponent result:  caf%C3%A9%20%26%20tea

Now it drops safely into ?q=caf%C3%A9%20%26%20tea without breaking the surrounding query string structure.

The + vs %20 Problem (and When It Matters)

HTML forms use application/x-www-form-urlencoded encoding by default. Under this scheme, spaces become + rather than %20. Most servers and frameworks handle both, but not all — and the difference matters in two situations:

  1. You're constructing a query string by hand for a server that follows the HTML form encoding standard (common in older PHP and Java apps). Use + for spaces.
  2. You're building a URL for a path segment (e.g. /search/green tea). Here + is a literal plus sign, not a space. Always use %20 in path segments.

According to the W3C HTML5 specification, the application/x-www-form-urlencoded format explicitly requires + for space, while RFC 3986 (the URI standard) uses %20. When you're talking to a REST API, prefer %20 unless the API documentation says otherwise — it's the unambiguous choice.

When I'm not sure what a given API expects, I paste the URL into URL Query String Parser & Builder and switch between %20 and + output modes to see exactly what each option generates before sending the request.

How to Safely Encode Parameters in JavaScript

The right pattern for building a query string from a JavaScript object:

// Input object
const params = {
  q: "café au lait",
  filter: "price<100 & rating>4",
  ref: "home page"
};

// Safe encoding
const queryString = Object.entries(params)
  .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
  .join("&");

// Output
// q=caf%C3%A9%20au%20lait&filter=price%3C100%20%26%20rating%3E4&ref=home%20page

And to decode it on the receiving end:

// Input query string (from URL search params)
const raw = "q=caf%C3%A9%20au%20lait&filter=price%3C100%20%26%20rating%3E4";

const decoded = Object.fromEntries(
  new URLSearchParams(raw)
);

// Output
// { q: "café au lait", filter: "price<100 & rating>4" }

URLSearchParams handles both %20 and + for spaces, and correctly decodes multi-byte UTF-8 sequences. It's the safest option in modern browsers and Node.js 10+.

One real-world input I tested: name=Ren%C3%A9+Magritte&year=1898. URLSearchParams decodes this as { name: "René Magritte", year: "1898" } — the %C3%A9 becomes é and the + becomes a space, both correctly.

Edge Cases That Bite in Production

Double-encoding is the most common mistake. If you receive a URL-encoded string and encode it again before storing or forwarding, you get %2520 instead of %25 (a literal %), and the downstream system sees a broken escape. Always decode first, then re-encode if needed.

Null bytes (%00) in query parameters are a security concern — some servers interpret them as string terminators, which can truncate or confuse parameter parsing. Sanitize inputs server-side; don't rely on the client to avoid them.

Emoji in query strings are valid after URL encoding. 🐈 encodes as %F0%9F%90%88 (four bytes in UTF-8). Most modern stacks handle this correctly, but if you're proxying through an older load balancer or WAF, test it. I ran into a WAF that rejected %F0 as a suspected attack pattern — the fix was an allow-list rule, not a change to the encoding.

Hash fragments never reach the server. If a user shares a URL like https://example.com/app#section=3, the #section=3 is purely client-side. Don't try to encode it into a query parameter expecting the server to read it.

For quick validation of any encoded URL, the URL Encoder / Decoder lets you paste a raw string and see both the encoded and decoded forms side by side — useful when a %XX sequence in a log doesn't match what you expect.

Decoding Safely: Don't Trust the Input

Decoding is where injection risks live. Never decode a query parameter and immediately insert it into HTML, SQL, or a shell command. Each context needs its own escaping layer on top of URL decoding:

  • HTML context: URL-decode the value, then HTML-encode it before rendering. <script> becomes &lt;script&gt;, not a running tag.
  • SQL context: URL-decode, then use parameterized queries — never string concatenation.
  • Shell context: URL-decode, then quote the variable or use an argument array.

A useful mental model: URL encoding is a transport encoding. It tells the HTTP layer how to read the bytes. Once the server hands you the decoded string, you're back in application territory and your normal injection defenses apply.

If you're dealing with URLs that have both query parameters and HTML-escaped characters (common when scraping or parsing redirects), the HTML Entities Encoder can help you sort out which layer of encoding applies to which characters.

A Quick Checklist Before You Ship

  • [ ] Using encodeURIComponent on each key and value separately, not on the full URL
  • [ ] Using URLSearchParams or a trusted library to parse incoming query strings
  • [ ] Not double-encoding values that arrive already encoded
  • [ ] Applying HTML/SQL/shell escaping after URL decoding, not instead of it
  • [ ] Testing with at least one multi-byte character (é, ñ, 中, 🐈) to catch UTF-8 issues

URL encoding is a small enough topic that it's easy to get overconfident about — and small enough that a single missed edge case cascades into broken user data, failed API calls, or security gaps. The rules themselves are stable; the mistakes happen when people mix the two JavaScript functions, forget which encoding scheme a given endpoint expects, or decode without re-escaping for the target context.


Made by Toolora · Updated 2026-06-09