Skip to content
DevPenh

URL Encoding Explained with Practical Examples

How percent-encoding works, encodeURIComponent vs encodeURI, plus signs vs %20, double encoding and decoding errors, with tested JavaScript, Python and curl examples.

Last reviewed

URL encoding (percent-encoding) is how arbitrary text is made safe to put in a URL. It looks trivial — %20 for a space — but choosing the wrong function, encoding twice, or confusing + with a space causes some of the most persistent bugs in web development. This guide explains the rules and shows the exact output of the common functions.

Examples were run with Node.js 24.21, Python 3.9.6 and curl 8.7.1. Try any of them in the URL encoder.

How percent-encoding works

A URL may only contain a limited set of ASCII characters, and some of those — : / ? # [ ] @ ! $ & ' ( ) * + , ; = — are reserved because they separate the parts of the URL. To include any other character, or a reserved character as data, the text is converted to UTF-8 bytes and each byte is written as % followed by two hexadecimal digits (RFC 3986).

  • A space (byte 0x20) becomes %20.
  • é is two UTF-8 bytes, so it becomes %C3%A9.
  • € is three bytes: %E2%82%AC. The emoji 👋 is four: %F0%9F%91%8B.

Letters, digits and - . _ ~ never need encoding. Hex digits are case-insensitive: %2F and %2f mean the same byte (curl, for example, writes lowercase).

encodeURIComponent vs encodeURI

JavaScript has two built-in functions, and they are for different jobs. With the same input:

const s = "price=10€ & tax/fee?";

encodeURIComponent(s); // "price%3D10%E2%82%AC%20%26%20tax%2Ffee%3F"
encodeURI(s);          // "price=10%E2%82%AC%20&%20tax/fee?"
  • encodeURIComponent encodes everything except A–Z a–z 0–9 - _ . ! ~ * ' ( ). Use it for a single value: one query parameter, one path segment.
  • encodeURI leaves reserved characters such as = & / ? alone, because it assumes you are passing a whole URL whose structure is already correct. Use it only to escape spaces and non-ASCII characters in a complete URL.

Using encodeURI on a value is a classic bug: in the output above, & and = survive unencoded, so a server would read tax/fee? as a separate parameter.

Plus signs, spaces and form encoding

HTML forms and URLSearchParams use a slightly different format, application/x-www-form-urlencoded, where a space is written as +:

new URLSearchParams({ q: "price=10€ & tax/fee?" }).toString();
// "q=price%3D10%E2%82%AC+%26+tax%2Ffee%3F"

That means the meaning of + depends on which decoder reads it:

decodeURIComponent("a+b");                     // "a+b"  (plus stays a plus)
new URLSearchParams("q=a+b").get("q");         // "a b"  (plus is a space)
new URLSearchParams("q=a%2Bb").get("q");       // "a+b"

Most servers decode query strings the form way, so a literal plus sign must be sent as %2B. If phone numbers like +855… or base64 values arrive with spaces in them, something put a raw + in the query string.

Building URLs safely

Never concatenate raw values into a URL. This looks fine but silently loses data:

const q = "tom & jerry";
new URL(`https://api.example.com/search?q=${q}`).searchParams.get("q");
// "tom "   (the & started a new parameter)

Let the URL API do the encoding instead. It encodes each value correctly, including the plus signs in c++:

const url = new URL("https://api.example.com/search");
url.searchParams.set("q", "red shoes & socks");
url.searchParams.set("tag", "c++");
url.href;
// "https://api.example.com/search?q=red+shoes+%26+socks&tag=c%2B%2B"

For path segments, encode each segment separately with encodeURIComponent, so a slash inside a value cannot create an extra path level:

"/files/" + encodeURIComponent("reports/2026 Q1.pdf");
// "/files/reports%2F2026%20Q1.pdf"

Double encoding

Encoding a value that is already encoded turns every % into %25:

encodeURIComponent(encodeURIComponent("café"));
// "caf%25C3%25A9"

The server decodes once and receives the literal text caf%C3%A9. If you see %25 in a URL, or search results stop matching for non-ASCII input, look for two layers that both encode — for example, your code calls encodeURIComponent and then an HTTP library encodes the parameters again. Encode in exactly one place, as close to the HTTP call as possible.

Decoding errors

JavaScript's decoders throw when a % is not followed by two hex digits, or when the bytes are not valid UTF-8:

decodeURIComponent("100%");  // URIError: URI malformed
decodeURIComponent("%C3");   // URIError: URI malformed (incomplete UTF-8 sequence)

Wrap decoding of untrusted input in try/catch. The URL encoder reports where the first malformed sequence is. Python is more forgiving, which can hide the problem:

from urllib.parse import unquote
unquote("100%")   # '100%'
unquote("%C3")    # '\ufffd'  (replacement character, no error)

The same operations in Python and curl

from urllib.parse import quote, quote_plus, urlencode

s = "price=10€ & tax/fee?"
quote(s)              # 'price%3D10%E2%82%AC%20%26%20tax/fee%3F'   (keeps "/" by default!)
quote(s, safe="")     # 'price%3D10%E2%82%AC%20%26%20tax%2Ffee%3F' (like encodeURIComponent)
quote_plus(s)         # 'price%3D10%E2%82%AC+%26+tax%2Ffee%3F'     (form style)
urlencode({"q": s, "tag": "c++"})
# 'q=price%3D10%E2%82%AC+%26+tax%2Ffee%3F&tag=c%2B%2B'

Note that Python's quote leaves / unencoded unless you pass safe="" — the same trap as encodeURI.

# curl encodes a query value for you with -G and --data-urlencode
curl -G https://api.example.com/search --data-urlencode "q=price=10€ & tax/fee?"
# requests: https://api.example.com/search?q=price%3d10%e2%82%ac+%26+tax%2ffee%3f

Quick reference

You are encodingJavaScriptPython
One query value or path segmentencodeURIComponent(v)quote(v, safe="")
A whole query string from key/value pairsnew URLSearchParams(obj).toString()urlencode(obj)
A complete URL with spaces or non-ASCIInew URL(str).href or encodeURI(str)quote(str, safe=":/?#[]@!$&'()*+,;=")
Decoding a query valuenew URLSearchParams(qs).get(k)parse_qs(qs) or unquote_plus(v)

When a request behaves strangely, paste the full URL into the URL parser on the URL encoder page to see every query parameter exactly as the server will decode it. For the wider debugging workflow, see How to debug API responses.

Part of the API debugging series. Start with How to Debug API Responses: A Practical Workflow.