Percent-Encoding URLs: When and How
Percent-encoding converts characters that are not allowed in a URL component into a %XX hexadecimal representation. Every byte of the UTF-8 representation of the character is encoded separately: the space character becomes %20, and the euro sign (€, U+20AC) becomes %E2%82%AC1. Encoding rules differ by component because characters that are safe in a path segment may be delimiters in a query string, requiring encoding only in that context.
Most URL-related bugs involving encoding fall into two categories: double-encoding (encoding an already-encoded string, turning %20 into %25202) and context mismatch (encoding a full URL string instead of individual values, which turns / into %2F and breaks routing). Using context-aware encoding functions prevents both.
Which characters to encode and where
Unreserved characters (A–Z, a–z, 0–9, hyphen, period, underscore, tilde) never require encoding in any URL component. Reserved characters have special meaning in URLs (:/?#[]@!$&'()*+,;=) and must be encoded when used as literal data rather than as delimiters1. Consequently, path segments may contain unreserved characters, :, @, !, $, &, ', (, ), *, +, ,, ;, and = without encoding, but spaces and most other special characters must be encoded as %20. Query parameter values must encode all reserved characters except ~ and the safe set. Building on this, fragments follow the same rules as query parameters, so the same encoding function can be applied to both components without modification.
Encoding functions by language and context
JavaScript provides two encoding functions for URL components: encodeURIComponent() encodes everything except unreserved characters2 and is correct for path segments and query parameter values. encodeURI() is less strict because it preserves :/?#@!$&'()*+,;=, so it is suitable for encoding a complete URL that is already partially formed but should not be re-encoded. For query parameter values in Python, urllib.parse.quote(value, safe='') encodes per RFC 3986 (%20 for spaces)3. Building on this, quote_plus(value) encodes for form data format (+ for spaces), which is the correct choice for HTML form submission query strings. Never use quote() on a complete URL string.
Common mistakes and double-encoding
Double-encoding occurs when an already-encoded value is encoded again: a%20space encoded again produces a%2520space. Always decode before re-encoding: URLDecoder.decode(value) in Java, unquote(value) in Python, decodeURIComponent(value) in JavaScript. Consequently, when a server receives a query parameter and passes it to another URL, decode the received value and re-encode it for the new context. Building on this, + in a query string means a space only in application/x-www-form-urlencoded format, while in an RFC 3986 URL + is a literal plus sign4. Using the wrong decoder produces subtle data corruption when values contain plus signs, so always verify which format the source URL uses before choosing a decoder.
Percent-encoding in the fragment component
The fragment component follows the same percent-encoding rules as the query string, but with one key difference: the application/x-www-form-urlencoded convention (+ for spaces) does not apply. A fragment of #q=hello+world means the literal string "hello+world", not "hello world". This distinction matters for single-page applications that store search state in the fragment: if your JavaScript reads window.location.hash and splits on + expecting spaces, you will misinterpret the user's input. Always use %20 for spaces in fragment values, and decode with decodeURIComponent() rather than a form-specific decoder.
Fragment encoding in OAuth implicit grants
The OAuth 2.0 implicit grant delivers the access token in the fragment: https://app.example.com/callback#access_token=abc123&token_type=Bearer5. The fragment is never sent to the server, which protects the token from appearing in server logs. However, the fragment is visible to any JavaScript on the page, including third-party analytics scripts. This is why the implicit grant is deprecated in favor of the authorization code flow with PKCE: the code flow delivers tokens via a back-channel POST that is invisible to page-level JavaScript. If you must use the implicit grant, ensure no third-party scripts have access to the callback page.
Because the token lives in the fragment only for the lifetime of the page, extract it immediately on load and clear the fragment with history.replaceState before any analytics or routing code runs. Treat the token as already exposed the moment it appears, since the fragment persists in browser history and can be read by any script that later loads on that origin. Removing it from the URL as early as possible limits the window in which a compromised library could capture it.
Encoding differences between URL components in practice
Each URL component has its own encoding context, and a character that is safe in one component may need encoding in another. The @ character is valid in the userinfo portion of the authority (user@host) but must be encoded as %40 in a path segment. The / character is a valid path delimiter but must be encoded as %2F inside a single path segment. The ? character starts the query component but is valid (and must be encoded as %3F) inside a path segment. Context-aware encoding functions handle these rules: encodeURIComponent() in JavaScript encodes everything except unreserved characters, making it safe for any component.
Encoding slashes in REST API paths
A common REST API design question is how to handle resource identifiers that contain slashes. A file path like documents/2026/report.pdf cannot appear directly in a URL path segment without being interpreted as three separate segments. The standard solution is to encode the slash as %2F: /files/documents%2F2026%2Freport.pdf. However, some web servers (Apache with AllowEncodedSlashes, nginx with merge_slashes) decode %2F before routing, which means the server sees the decoded path. Test your server's behavior with encoded slashes before relying on this pattern, or use a query parameter instead: /files?path=documents/2026/report.pdf.
URL encoding in email and mailto links
Mailto links require percent-encoding for special characters in the subject and body parameters. A mailto: link with a subject containing an ampersand or question mark breaks the URL structure unless those characters are encoded: mailto:[email protected]?subject=Hello%20%26%20Goodbye&body=Line%201%0ALine%2026. The %0A encodes a newline in the body. Not all mail clients handle complex mailto links correctly; some truncate at the first &, others ignore the body parameter entirely. For reliable email composition, keep mailto links simple (address only) and use a contact form for messages that require formatting.
Encoding URLs in HTML attributes
When embedding URLs in HTML attributes (href, src, action), the URL must be both percent-encoded (for URL syntax) and HTML-escaped (for HTML syntax). An ampersand in a query parameter must be percent-encoded as %26 for the URL layer, and the entire attribute value must escape & as & for the HTML layer, since a raw ampersand breaking inside an href attribute is what happens when only one of those two layers gets applied. The correct form is: <a href="https://example.com/search?q=hello%20world&page=2">. The & in the HTML source becomes & when the browser parses the HTML, and the resulting URL contains the literal & that separates query parameters. Forgetting the HTML-level escaping is one of the most common bugs in hand-written HTML with query parameters7.
When to use this
Apply percent-encoding whenever you interpolate user-supplied values, file names, or non-ASCII text into a URL component such as a path segment, query parameter value, or header value that contains a URL.
Examples
Encode path segment and query value correctly in JavaScript
// Wrong: encodeURI applied to a component value leaves & and = unencoded
const category = "food & drink";
const url = `/items?category=${encodeURI(category)}`;
// → /items?category=food%20&%20drink (& breaks the query!) // Correct: encodeURIComponent encodes & and = in values
const url2 = `/items?category=${encodeURIComponent(category)}`;
// → /items?category=food%20%26%20drink Avoid double-encoding a URL parameter in Python
# Wrong: value already encoded encoding again produces %25 from urllib.parse import quote received = "hello%20world" # came from a URL query param encoded = quote(received, safe="") # → "hello%2520world" (double-encoded!)
# Correct: decode first then re-encode for the new URL from urllib.parse import unquote, quote decoded = unquote(received) # → "hello world" re_encoded = quote(decoded, safe="") # → "hello%20world"
- 1.
T. Berners-Lee, R. Fielding, and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax," RFC 3986, IETF, January 2005. https://www.rfc-editor.org/rfc/rfc3986.html
- 2.
Mozilla Developer Network, "encodeURIComponent() — JavaScript," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
- 3.
Python Software Foundation, "urllib.parse — Parse URLs into components," docs.python.org, accessed June 2026. https://docs.python.org/3/library/urllib.parse.html
- 4.
WHATWG, "URL Standard — application/x-www-form-urlencoded," url.spec.whatwg.org, accessed June 2026. https://url.spec.whatwg.org/#application-x-www-form-urlencoded
- 5.
D. Hardt, "The OAuth 2.0 Authorization Framework," RFC 6749, IETF, October 2012. https://datatracker.ietf.org/doc/html/rfc6749
- 6.
M. Duerst, L. Masinter, and J. Zawinski, "The 'mailto' URI Scheme," RFC 6068, IETF, October 2010. https://www.rfc-editor.org/rfc/rfc6068.txt
- 7.
Stack Overflow, "Do I encode ampersands in <a href...>?," stackoverflow.com, accessed June 2026. https://stackoverflow.com/questions/3705591/do-i-encode-ampersands-in-a-href