By the DevDash Team · Last updated July 2026
URL Encoding Explained: Percent-Encoding, encodeURIComponent, and Common Pitfalls
URL encoding is one of those things every developer encounters but few take the time to understand properly. The result is a class of bugs that show up as broken links, mangled query parameters, and mysterious 400 errors — all caused by encoding the wrong characters, encoding in the wrong place, or encoding twice. This guide covers how percent-encoding actually works, which characters need it, and how to use it correctly across JavaScript, Python, PHP, and the command line.
What Is URL Encoding and Why Does It Exist?
URLs can only contain a limited set of characters from the ASCII character set. When a URL needs to include characters outside that set — spaces, international characters, or symbols that have special meaning within URLs themselves — those characters must be converted into a format that is safe to transmit. This conversion process is called URL encoding, formally known as percent-encoding as defined in RFC 3986.
The problem URL encoding solves is structural ambiguity. Consider a search query like fish & chips. If you drop that directly into a URL as ?q=fish & chips, the browser interprets & as a parameter separator and chips as the start of a new parameter. The server receives q=fish and a stray parameter named chips with no value. URL encoding fixes this by converting the ampersand to %26, producing ?q=fish%20%26%20chips — unambiguous and correct.
Which Characters Need Encoding?
RFC 3986 divides characters into three categories that determine when encoding is required.
Unreserved characters (never encode these)
These characters are always safe in any part of a URL and should never be percent-encoded:
A-Z a-z 0-9 - _ . ~
That's it — 66 characters total. Encoding these would not cause an error (a compliant parser will decode them), but it produces unnecessarily ugly URLs and wastes bytes.
Reserved characters (encode when used as data)
These characters have structural meaning in URLs. They are safe when used for their intended purpose (for example, ? to start a query string) but must be encoded when they appear as literal data inside a component:
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
Everything else (always encode)
All other characters — spaces, control characters, non-ASCII characters like accented letters, CJK characters, and emoji — must always be percent-encoded in URLs.
How Percent-Encoding Works: The Mechanism
The encoding process follows three steps for each character that needs encoding:
1. Convert the character to its UTF-8 byte sequence. ASCII characters produce a single byte. Multi-byte characters (accented letters, CJK, emoji) produce two to four bytes.
2. Write each byte as a percent sign followed by two uppercase hexadecimal digits. The hex digits represent the byte value.
3. Concatenate the results.
Here are some examples showing how this works in practice:
Character UTF-8 Bytes Percent-Encoded ───────── ─────────── ─────────────── space 0x20 %20 & 0x26 %26 ñ 0xC3 0xB1 %C3%B1 € 0xE2 0x82 0xAC %E2%82%AC 🚀 0xF0 0x9F 0x9A 0x80 %F0%9F%9A%80
Notice how a single emoji becomes 12 characters when percent-encoded. This is why URLs with international characters or emoji can get very long.
Common Encoded Characters Reference
These are the characters you will encounter most frequently in percent-encoded URLs. Bookmark this table — you will reach for it more often than you expect.
| Character | Encoded | Why It Matters |
|---|---|---|
| Space | %20 (or + in forms) | Most common encoding — see %20 vs + section |
| & | %26 | Separates query parameters |
| = | %3D | Separates parameter name from value |
| ? | %3F | Starts the query string |
| # | %23 | Starts the fragment identifier |
| / | %2F | Path separator — encode only in values |
| @ | %40 | Used in userinfo (user@host) |
| + | %2B | Literal plus sign (not a space) |
| % | %25 | The escape character itself |
| : | %3A | Scheme separator (http:) and port (:443) |
| ! | %21 | Sub-delimiter, sometimes encoded |
| ' | %27 | Often encoded to prevent injection |
Space Encoding: %20 vs + and Why Both Exist
The space character has two valid encodings in URLs, and mixing them up is a constant source of confusion. The short answer: %20 is the standard percent-encoding from RFC 3986. The plus sign + comes from the older application/x-www-form-urlencoded format defined by HTML for form submissions.
In practice: use %20 everywhere except when constructing application/x-www-form-urlencodedbodies (HTML form POST data). JavaScript's encodeURIComponent() produces %20. The URLSearchParams API produces +. Both are correct in their respective contexts, but they are not interchangeable in all URL positions — a + in the path segment of a URL is a literal plus sign, not a space.
encodeURI vs encodeURIComponent: The Critical Difference
JavaScript provides two built-in functions for URL encoding, and using the wrong one is one of the most common encoding bugs in web development.
encodeURI — for complete URLs
encodeURI() encodes a full URL string. It leaves characters that are structurally meaningful in URLs untouched: : / ? # [ ] @ ! $ & ' ( ) * + , ; =. It only encodes characters that are never valid in any URL component.
encodeURI("https://example.com/search?q=fish & chips")
// "https://example.com/search?q=fish%20&%20chips"
// ^ & is NOT encoded — BUG!In this example, the & inside the query value is left as-is, breaking the parameter structure. This is why encodeURI is rarely the right choice for user-generated content.
encodeURIComponent — for individual values
encodeURIComponent() encodes a single URL component and encodes all reserved characters. This is what you want when building query strings:
const query = "fish & chips"; const url = "https://example.com/search?q=" + encodeURIComponent(query); // "https://example.com/search?q=fish%20%26%20chips" // ^ & IS encoded — correct
The rule of thumb
Use encodeURIComponent() for encoding parameter names and values. Use encodeURI() only when you have a complete URL that just needs non-ASCII characters encoded (rare in practice). For building query strings from multiple parameters, use the URLSearchParams API, which handles encoding automatically:
const params = new URLSearchParams({
q: "fish & chips",
category: "food/dining",
price: "£10-£20"
});
params.toString();
// "q=fish+%26+chips&category=food%2Fdining&price=%C2%A310-%C2%A320"URL Encoding in Python, PHP, and the Command Line
Python
Python's urllib.parse module provides two encoding functions with different behaviors:
from urllib.parse import quote, quote_plus, urlencode
# quote() — for path segments (preserves /)
quote("café menu/drinks") # "caf%C3%A9%20menu/drinks"
# quote_plus() — for query values (spaces become +)
quote_plus("fish & chips") # "fish+%26+chips"
# urlencode() — for full query strings from a dict
urlencode({"q": "fish & chips", "page": "1"})
# "q=fish+%26+chips&page=1"PHP
PHP has a similar split between form-style and RFC-style encoding:
// urlencode() — spaces become + (form-style)
urlencode("fish & chips"); // "fish+%26+chips"
// rawurlencode() — spaces become %20 (RFC 3986)
rawurlencode("fish & chips"); // "fish%20%26%20chips"
// http_build_query() — for full query strings
http_build_query(["q" => "fish & chips", "page" => "1"]);
// "q=fish+%26+chips&page=1"Command line with curl
When using curl, the --data-urlencode flag handles encoding automatically for POST data. For GET requests, you need to encode values manually or use the --data-urlencode flag with -G:
# POST with automatic encoding curl -X POST https://api.example.com/search \ --data-urlencode "q=fish & chips" # GET with automatic encoding (-G converts POST data to query string) curl -G https://api.example.com/search \ --data-urlencode "q=fish & chips" # Requests: https://api.example.com/search?q=fish%20%26%20chips
URL Anatomy: What Gets Encoded Where
A URL has five major parts, and each part has different encoding rules. Understanding this structure is the key to avoiding encoding mistakes:
https://user:pass@example.com:8080/path/to/page?key=value&k2=v2#section └─┬──┘ └───┬────┘└────┬─────┘└┬─┘└─────┬──────┘└─────┬────────┘└──┬───┘ scheme userinfo host port path query fragment
Scheme (https): Never encoded. Always plain ASCII.
Host: Internationalized domain names use Punycode (see below), not percent-encoding.
Path: Encode everything except unreserved characters and the / separator. A literal / within a path segment must be encoded as %2F.
Query: Encode parameter names and values individually. Leave the structural & and = characters unencoded.
Fragment: Same rules as path segments. Fragments are never sent to the server — they are handled entirely by the browser.
The most common mistake is encoding an entire URL with encodeURIComponent(), which turns https:// into https%3A%2F%2F and breaks the URL completely. Encode components, not the entire URL.
Common Bugs Caused by Improper URL Encoding
Double encoding
The most frequent encoding bug. It happens when an already-encoded string passes through an encoding function a second time:
// First encoding: correct
encodeURIComponent("hello world") // "hello%20world"
// Second encoding: bug — %20 becomes %2520
encodeURIComponent("hello%20world") // "hello%2520world"
// ^^^ the % sign got encoded to %25The fix: encode each value exactly once, at the point where you assemble the URL. If you receive a URL from an external source, do not re-encode it unless you have decoded it first.
Encoding the entire URL instead of just values
Using encodeURIComponent() on a full URL destroys its structure. The colons, slashes, question marks, and ampersands that define the URL get encoded and the result is unusable. Build URLs by encoding individual components and concatenating them, or use the URL constructor in JavaScript:
const url = new URL("https://example.com/search");
url.searchParams.set("q", "fish & chips");
url.searchParams.set("lang", "en");
url.toString();
// "https://example.com/search?q=fish+%26+chips&lang=en"Forgetting to encode at all
This produces URLs that look correct in a browser (which auto-encodes the address bar) but break when used in HTTP requests, redirects, or log parsing. Always encode programmatically — never rely on implicit encoding by the browser.
Mishandling + as a space
A + in a query string is a space, but a + in the URL path is a literal plus sign. If your backend decodes the path using form-decoding logic, plus signs get silently replaced with spaces. This is a particularly insidious bug because it only manifests with input that contains actual plus signs.
Query String Construction Best Practices
Building query strings by manual string concatenation is error-prone. Use the platform's built-in APIs whenever possible:
// JavaScript — URLSearchParams (recommended)
const params = new URLSearchParams();
params.set("q", userInput);
params.set("page", "1");
const url = `https://api.example.com/search?${params}`;
// Python — urllib.parse.urlencode
from urllib.parse import urlencode
params = urlencode({"q": user_input, "page": "1"})
url = f"https://api.example.com/search?{params}"
// PHP — http_build_query
$params = http_build_query(["q" => $userInput, "page" => "1"]);
$url = "https://api.example.com/search?{$params}";These APIs handle encoding, escaping, and the & separators automatically. They also handle edge cases like empty values and array parameters that manual construction gets wrong.
When URL Decoding Is Needed
Most web frameworks automatically decode URL parameters before your application code sees them. You need to manually decode in specific situations:
Reading server logs. Access logs typically record the raw, encoded URL. To understand what a user actually requested, %E2%9C%93needs to be decoded back to the original character. DevDash's URL encoder/decoder handles this instantly.
Debugging redirects. Redirect chains often pass URLs as query parameter values, which means the URL gets encoded. A redirect to ?next=https%3A%2F%2Fexample.com%2Fdashboard is just ?next=https://example.com/dashboard after decoding.
Inspecting webhook payloads. Webhooks from payment providers, form services, and analytics platforms often send URL-encoded payloads. You need to decode them to read the actual data.
Working with analytics and tracking URLs. UTM parameters and click-tracking URLs frequently contain encoded values that need decoding for analysis.
International Characters, IDN, and Punycode
International characters in URLs are handled differently depending on where they appear.
In the path and query string: International characters are percent-encoded using their UTF-8 byte representation. The word cafe with an accent — café — becomes caf%C3%A9 in the URL. Browsers display the decoded version in the address bar for readability, but the encoded version is what gets sent over the network.
In the domain name: International domain names (IDN) use a completely different system called Punycode, defined in RFC 3492. Instead of percent-encoding, the domain is converted to an ASCII-compatible encoding with an xn-- prefix. For example, münchen.de becomes xn--mnchen-3ya.de. This happens at the DNS level and is transparent to most application code.
When building applications that accept international input — search queries in Japanese, user names with accents, addresses with non-Latin scripts — the standard percent-encoding functions in JavaScript, Python, and PHP handle the UTF-8 conversion automatically. You do not need to manually convert to UTF-8 bytes first. Just call encodeURIComponent() and it produces the correct result for any Unicode input.
URL Encoding vs. Base64 Encoding
URL encoding and Base64 encoding solve different problems and are not interchangeable. URL encoding makes individual characters safe for URLs by converting them to %XX sequences. Base64 converts arbitrary binary data into a text string using a 64-character alphabet.
Sometimes you need both: when passing binary data (like an image or a cryptographic signature) as a URL parameter, you first Base64-encode the binary data, then URL-encode the resulting Base64 string — or use the URL-safe Base64 variant (base64url) that replaces + with - and / with _ to avoid conflicts with URL syntax.
Encode and Decode URLs Instantly
Paste a URL or query string to encode, decode, or parse it into its components. Client-side processing — your data never leaves your browser.
Open URL Encoder/Decoder