How to Encode and Decode a URL: JavaScript, Python, PHP and Why Each Gives a Different Result

URL encoding replaces characters that are unsafe or meaningful in a URL with `%` followed by two hex digits. Every language has a function for it, and they do not agree. Encode the same string with JavaScript's `encodeURIComponent`, Python's `quote_plus` and PHP's `urlencode`, and you get three different results, all "correct" for their own purpose. Choosing the right function for the part of the URL you are building is most of the job.

Encode or decode online

Paste a URL or a single value to encode it, or paste an encoded URL such as https://example.com/search?q=caf%C3%A9%20menu to decode it back to café menu. Decoding is the common case in debugging: tracking links, OAuth redirect URLs and logged requests are often encoded several layers deep.

Decode one layer at a time. If the output still contains %25, the input was encoded twice, because %25 is the encoding of % itself. Decode again only if you know the value was double-encoded on purpose.

caf%25C3%25A9  ->  caf%C3%A9  ->  café
(encoded twice)    (once)        (plain)

The same string, five functions, five results

This is the input a b+c/d?e=f&g~h*i!j'k é passed through the most common functions. It is real output from Node.js, Python and PHP.

encodeURIComponent (JS)   a%20b%2Bc%2Fd%3Fe%3Df%26g~h*i!j'k%20%C3%A9
encodeURI (JS)            a%20b+c/d?e=f&g~h*i!j'k%20%C3%A9
URLSearchParams (JS)      a+b%2Bc%2Fd%3Fe%3Df%26g%7Eh*i%21j%27k+%C3%A9
quote (Python)            a%20b%2Bc/d%3Fe%3Df%26g~h%2Ai%21j%27k%20%C3%A9
quote_plus (Python)       a+b%2Bc%2Fd%3Fe%3Df%26g~h%2Ai%21j%27k+%C3%A9
rawurlencode (PHP)        a%20b%2Bc%2Fd%3Fe%3Df%26g~h%2Ai%21j%27k%20%C3%A9
urlencode (PHP)           a+b%2Bc%2Fd%3Fe%3Df%26g%7Eh%2Ai%21j%27k+%C3%A9

Three differences matter. Spaces become %20 in URL-component encoding and + in HTML-form encoding. encodeURI leaves / ? = & alone, because it assumes you are passing a whole URL whose structure must survive. And functions differ on ! ' ( ) * ~: they are all allowed in URLs, so leaving them unencoded is valid, but it breaks naive string comparisons and some signature schemes, such as OAuth 1.0 and AWS SigV4, which require strict RFC 3986 encoding.

JavaScript: which function when

// One value going into a query string or path segment
const url = `https://api.example.com/search?q=${encodeURIComponent(query)}`;

// Many parameters: let URLSearchParams do it (form encoding, space = +)
const params = new URLSearchParams({ q: 'café menu', page: '2' });
fetch(`https://api.example.com/search?${params}`);

// Build and normalise a whole URL
const u = new URL('https://example.com/a b/é?q=a b');
u.href;   // 'https://example.com/a%20b/%C3%A9?q=a%20b'

// Decoding
decodeURIComponent('caf%C3%A9%20menu');          // 'café menu'
new URLSearchParams('q=caf%C3%A9+menu').get('q'); // 'café menu' (+ -> space)

encodeURI is rarely the right choice. It does not encode &, = or ?, so a value containing them silently adds parameters or cuts the URL short. Use encodeURIComponent for values and the URL API for whole URLs. The old escape() and unescape() functions are deprecated and do not use UTF-8, so avoid them.

decodeURIComponent throws URIError: URI malformed on input such as 100% or %E0%A4%A. Wrap it in try/catch when the input comes from users or logs.

Python and PHP

from urllib.parse import quote, quote_plus, urlencode, unquote, unquote_plus

quote('a b/c')                 # 'a%20b/c'    — note: / is kept (safe='/')
quote('a b/c', safe='')        # 'a%20b%2Fc'  — encode / too, for a single segment
quote_plus('a b&c')            # 'a+b%26c'    — form style
urlencode({'q': 'café', 'page': 2})   # 'q=caf%C3%A9&page=2'

unquote('a+b%20c')             # 'a+b c'      — + stays +
unquote_plus('a+b%20c')        # 'a b c'      — + becomes space
<?php
rawurlencode('a b');       // 'a%20b'  — RFC 3986, use for path segments
urlencode('a b');          // 'a+b'    — form encoding, use for query values
http_build_query(['q' => 'café', 'page' => 2]);   // 'q=caf%C3%A9&page=2'
rawurldecode('a+b');       // 'a+b'
urldecode('a+b');          // 'a b'

Python's quote keeps / by default. That is right for a path and wrong for a value that happens to contain a slash, such as a filename or a Base64 string: pass safe=''. In PHP, the rule of thumb is rawurlencode for anything in the path and urlencode or http_build_query for query strings.

The + versus %20 rule

In the path, + is a literal plus sign and a space must be %20. In a query string, HTML forms (application/x-www-form-urlencoded) encode spaces as +, and most server frameworks decode + as a space there. A literal plus in a query value must therefore be sent as %2B.

This is behind one of the most common URL bugs: a phone number +44 20 7946 0958 or a Base64 value ab+cd== put into a query string without encoding arrives with spaces where the + signs were. The fix is to encode the value with encodeURIComponent or URLSearchParams, both of which turn + into %2B.

When decoding, match the function to the context. decodeURIComponent leaves + alone, which is correct for paths and wrong for form data. Use URLSearchParams or unquote_plus for query strings.

Decoding safely

Decode once, at the boundary where the value enters your code; most frameworks already do this for route parameters and query strings. Decoding again later is a real vulnerability class. If a security check runs on the once-decoded value and the application decodes a second time, %252e%252e%252f passes the check as %2e%2e%2f and then becomes ../ for path traversal.

Validate after decoding, not before. A path check against the encoded string misses %2F, and a check for <script> misses %3Cscript%3E.

Non-UTF-8 input still exists in old systems. %E9 alone is é in Latin-1 but invalid UTF-8, and it makes JavaScript throw or Python produce a replacement character (unquote uses errors='replace' by default). If you must accept it, decode with an explicit legacy encoding.

Java, Go and C#

The same split between component encoding and form encoding exists in other languages, and the function names do not always make it obvious which one you are calling.

// Java — URLEncoder is FORM encoding: space -> +
URLEncoder.encode("a b", StandardCharsets.UTF_8);      // "a+b"

// Go
url.QueryEscape("a b")                                 // "a+b"   (query values)
url.PathEscape("a b")                                  // "a%20b" (path segments)

// C#
Uri.EscapeDataString("a b");                           // "a%20b"
WebUtility.UrlEncode("a b");                           // "a+b"

Java's URLEncoder is the one that catches people out: despite its name it implements HTML form encoding, so its output is wrong for path segments. Replace + with %20 afterwards, or use a URI builder, when you encode a path.

Command line

# Encode a value with Python (available almost everywhere)
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' 'a b/é'
# a%20b%2F%C3%A9

# Decode
python3 -c 'import sys, urllib.parse; print(urllib.parse.unquote_plus(sys.argv[1]))' 'caf%C3%A9+menu'

# Let curl encode query parameters for you
curl -G https://api.example.com/search --data-urlencode 'q=café & crème'

Sources and further reading

RFC 3986 defines percent-encoding and the reserved and unreserved characters. The WHATWG URL Standard defines URL, URLSearchParams and the application/x-www-form-urlencoded serialiser used by browsers. MDN documents encodeURIComponent, encodeURI and URIError. The Python urllib.parse and PHP rawurlencode documentation describe their encoding rules. OWASP describes double-encoding attacks.

Frequently asked questions

How do I decode a URL?

Paste it into a URL decoder, or use `decodeURIComponent()` in JavaScript, `urllib.parse.unquote()` in Python or `rawurldecode()` in PHP. For query strings where `+` means a space, use `URLSearchParams`, `unquote_plus()` or `urldecode()`.

What is the difference between encodeURI and encodeURIComponent?

`encodeURI` encodes a whole URL and leaves structural characters such as `/ ? & = #` unchanged. `encodeURIComponent` encodes a single value and escapes those characters too. Use `encodeURIComponent` for anything you insert into a URL.

Should a space be encoded as + or %20?

`%20` is always correct. `+` means a space only in `application/x-www-form-urlencoded` query strings; in a path it is a literal plus sign. When in doubt, use `%20`.

How do I URL encode a string in Python?

Use `urllib.parse.quote(s, safe='')` for a single value, so that `/` is encoded too, or `urllib.parse.urlencode()` to build a whole query string from a dict, which writes spaces as `+`. `unquote()` and `unquote_plus()` decode the two styles.

Why does decodeURIComponent throw URI malformed?

The input contains a `%` not followed by two hex digits, or bytes that are not valid UTF-8. Catch the `URIError`, and check whether the input was truncated or uses a legacy encoding.

What is the difference between urlencode and rawurlencode in PHP?

`urlencode` uses form encoding (space becomes `+`, and `~` becomes `%7E`). `rawurlencode` follows RFC 3986 (space becomes `%20`, and `~` is kept). Use `rawurlencode` for paths and `urlencode` or `http_build_query` for query strings.