Skip to content
Devtoolry
Security & Encoding

URL Encoding Explained: Percent-Encoding, encodeURIComponent and Common Bugs

How percent-encoding works, when to use encodeURIComponent vs encodeURI, why + sometimes means a space, and how to fix double encoding and broken query strings.

By Avinash KoriPublished 5 min read

A search query with spaces, an ampersand and a slash, shown before and after percent-encoding

A user searches your site for Tom & Jerry. The link you build is /search?q=Tom & Jerry, and your server receives a search for Tom plus a mysterious empty parameter called Jerry. Nothing crashed, nothing was logged, and the results are wrong.

That's a URL encoding bug, and it's one of the most common bugs on the web. This guide explains how percent-encoding works, which function to use in each situation, and how to diagnose the classic problems: spaces, plus signs, double encoding and Unicode.

Why URLs need encoding

A URL is made of parts, and certain characters separate those parts:

Output
https://example.com/path/to/page?key=value&other=1#section

/ separates path segments, ? starts the query, & separates parameters, = separates a name from its value, and # starts the fragment. These are reserved characters. If one of them appears inside a value, it must be encoded, or the URL means something different. Spaces, quotes, non-ASCII letters and control characters aren't allowed at all and must always be encoded.

How percent-encoding works

Percent-encoding replaces a byte with % followed by its two-digit hexadecimal value. A space is byte 0x20, so it becomes %20. An ampersand is 0x26, so it becomes %26.

For non-ASCII characters, the text is first converted to UTF-8 bytes, and each byte is encoded:

Output
é  → UTF-8 bytes C3 A9       → %C3%A9
🚀 → UTF-8 bytes F0 9F 9A 80 → %F0%9F%9A%80

So café 🚀 becomes caf%C3%A9%20%F0%9F%9A%80. Unreserved characters (A–Z a–z 0–9 - _ . ~) are never encoded.

You can try any value in the URL Encoder/Decoder, and see how a full URL splits into parts with the URL Parser.

encodeURIComponent vs encodeURI

JavaScript has two encoding functions, and choosing the wrong one is behind many bugs. Here's what they do with the same input:

JavaScript
const value = 'Tom & Jerry / 50% off?';

encodeURIComponent(value); // "Tom%20%26%20Jerry%20%2F%2050%25%20off%3F"
encodeURI(value);          // "Tom%20&%20Jerry%20/%2050%25%20off?"

encodeURIComponent encodes everything except unreserved characters (and ! * ' ( )). It's meant for a single piece of a URL: a query value, a path segment. This is the one you want almost every time.

encodeURI assumes you're giving it a complete URL, so it leaves the reserved characters & / ? = # : alone. That's fine for fixing spaces in a whole URL, but disastrous for a value: the & and ? above survive and break the query string.

Rule: build URLs from parts, and encode each part with encodeURIComponent. Never use encodeURI on a value.

Even better: let the URL API do it

Modern JavaScript has URL and URLSearchParams, which encode correctly without you choosing a function:

JavaScript
const url = new URL('https://example.com/search');
url.searchParams.set('q', 'C++ & Rust');
url.searchParams.set('page', '2');

url.href; // "https://example.com/search?q=C%2B%2B+%26+Rust&page=2"

Reading is just as easy, and decoding is automatic:

JavaScript
new URL(location.href).searchParams.get('q'); // "C++ & Rust"

Python's equivalent is urllib.parse.urlencode({'q': 'C++ & Rust'}), which gives q=C%2B%2B+%26+Rust. Most languages and HTTP libraries have something similar, so you rarely need to call an encoding function directly.

The plus sign problem

Notice that URLSearchParams wrote the space as +, not %20. This comes from HTML forms: the application/x-www-form-urlencoded format, used when a form is submitted, encodes spaces as +. In query strings, + and %20 therefore both commonly mean "space".

This causes two opposite bugs:

  • A literal plus becomes a space. If you build ?q=C++ by hand, a form-style parser reads C (C and two spaces). The plus must be encoded as %2B.
  • A plus isn't decoded. decodeURIComponent('a+b') returns a+b, because it only handles percent escapes. new URLSearchParams('q=a+b').get('q') returns a b.

Outside the query string, in the path, + is just a plus. /files/a+b.txt is a file called a+b.txt. So: decode query strings with a query-string parser, and decode paths with decodeURIComponent.

Double encoding

Double encoding happens when an already-encoded value is encoded again. The % itself gets encoded as %25:

Output
"my file"  →  my%20file  →  my%2520file

The server decodes once and gets my%20file: a literal percent sign, a 2 and a 0. Symptoms are %2520, %253A or %252F in your logs, and "file not found" for files that exist.

It usually happens when two layers each think they're responsible for encoding: you call encodeURIComponent, then pass the result to an HTTP library that encodes query parameters again. Or a redirect URL is encoded, stored, and encoded again when used. The fix is to encode exactly once, at the point where the URL is assembled, and to pass raw values everywhere else.

To diagnose, decode the value repeatedly until it stops changing: if it takes more than one decode to get readable text, something encoded it twice.

Malformed percent sequences

A % that isn't followed by two hex digits is invalid:

JavaScript
decodeURIComponent('100%'); // URIError: URI malformed

This often comes from user input like "50% off" that was put into a URL without encoding. Encode it properly (50%25%20off). On the server side, wrap decoding in a try/catch and return a 400 response rather than crashing.

Security notes

URL encoding is about correctness, but getting it wrong has security consequences:

  • Open redirects. A ?next= parameter that is decoded and followed without validation lets attackers send users to any site. Allow only relative paths or a list of known hosts.
  • Path traversal. %2e%2e%2f decodes to ../. Validate paths after decoding, and never build file paths from URL segments without normalising them.
  • Encoding is not escaping for HTML. A percent-encoded value placed into a page must still be HTML-escaped. Different contexts need different escaping; decoding a URL value and inserting it into the DOM with innerHTML is an XSS bug.

And as with Base64, encoding hides nothing: tokens and passwords in URLs end up in browser history, server logs and Referer headers. Send secrets in headers or request bodies instead.

A checklist for building URLs

  1. Use URL and URLSearchParams (or your language's URL builder) instead of string concatenation.
  2. If you must encode manually, use encodeURIComponent for each value — never encodeURI.
  3. Encode once, at the moment the URL is assembled.
  4. Encode literal + as %2B in query values; decode query strings with a form-aware parser.
  5. Validate redirect targets and file paths after decoding.
  6. When something looks wrong, paste the URL into the URL Parser to see exactly how each part and parameter is read.

Summary

Percent-encoding turns unsafe characters into % plus their UTF-8 bytes in hex, so values can't be confused with the URL's structure. Use encodeURIComponent (or, better, URLSearchParams) for values, remember that + means a space in query strings, and encode exactly once to avoid %2520. Most URL bugs disappear when URLs are built from parts by a real URL API.

Written by

Avinash Kori

Founder of Devtoolry

Avinash is a full-stack web developer from Surat, India, with eight years of experience in PHP, Laravel and Angular. He built Devtoolry so developers can solve everyday problems quickly with free tools that run entirely in the browser, and writes practical guides on the formats, encodings and techniques behind them.

Try it yourself

The free tools used in this article. They run in your browser, so nothing you paste is uploaded.