URL Encode / Decode
Percent-encode and decode URLs — and see which of the three encodings you actually need.
the encoded string appears here
Three encodings, not two
Percent-encoding turns characters that cannot live in a URL into a percent sign
followed by two hex digits: a space becomes %20, an accented
é becomes %C3%A9 as its UTF-8 bytes. That part is
settled. What gets left out of the usual explanation is that how you encode
differs by what the string is, and picking wrong is the bug. Measured on the same
input — a b+c/d?e=f&g — the three encodings give three
different answers:
| component | encodeURIComponent | a%20b%2Bc%2Fd%3Fe%3Df%26g |
| full URI | encodeURI | a%20b+c/d?e=f&g |
| form data | URLSearchParams | a+b%2Bc%2Fd%3Fe%3Df%26g |
The two traps are structural. encodeURI leaves + / ? = &
alone because in a whole URL they are syntax — and used on a value it
silently splinters that value into extra query parameters at the far end. Form
encoding turns a space into + and a literal + into
%2B, which is the entire "my plus sign became a space" genre. This page
never guesses which one you meant: the mode control is always visible, in words, and
the decoder resolves a pasted + according to the mode you selected, with
a note saying which reading you got.
The breakdown, and what an interpreter does to your URL
The second intent this page serves is "what is in this ugly link". Paste a
tracking URL and the breakdown below the tool splits it into scheme, host, each path
segment and each query pair, raw beside decoded — with repeated keys preserved
in order, an IDN host shown as both punycode and Unicode, and a password's presence
reported without its value. It decodes each part separately because
new URL() is deliberately lenient: it keeps %ZZ in a path
where decodeURIComponent throws, so one bad segment is reported in its
own row instead of swallowing the whole URL. A query value that is itself a URL —
an analytics redirect chain is built entirely out of those — offers to break
it down, one level deep and no further, because following the chain where it
goes requires a network request this page is forbidden from making, and saying so is
the honest answer to that one.
Two of the values that turn up in those rows have their own page. A segment that is
a run of A-Za-z0-9-_ with no vowel pattern is usually base64url, and
the base64 page decodes it without mangling
non-ASCII the way a Latin-1 atob does. A value with two dots in it
is usually a token: an OAuth callback and a magic link both arrive with a JWT
percent-encoded inside a query parameter, so decode it here and
read the claims on the jwt-debugger page — the
exp it reports is a
Unix timestamp, which has a page too.
Do it without this tool
The same three-way split exists in every language worth using, and the worst of the traps recur in each. First, the JavaScript that this page's mode control names.
// three encodings for three jobs — picking wrong is the bug
encodeURIComponent('a b+c/d?e=f&g') // 'a%20b%2Bc%2Fd%3Fe%3Df%26g' a single value
encodeURI('a b+c/d?e=f&g') // 'a%20b+c/d?e=f&g' a whole URL
new URLSearchParams([['k', 'a b+c']]).toString().slice(2) // 'a+b%2Bc' form data
// decodeURIComponent does NOT turn + into a space (that's form territory):
decodeURIComponent('a+b') // 'a+b'
new URLSearchParams('k=a+b').get('k') // 'a b'
from urllib.parse import quote, quote_plus, urlencode
quote("a b+c") # 'a%20b+c' — safe defaults to '/', so '/' is NOT escaped
quote("a b+c", safe="")
quote_plus("a b+c") # 'a+b%2Bc' — space becomes +, a literal + becomes %2B
urlencode({"q": "a b+c"}) # 'q=a+b%2Bc'
# The trap: quote("a/b") == 'a/b'. If a path segment sneaks in, it sails through
# unescaped and splits the route. Pass safe="" unless you are encoding a whole path.
# decode a path from an access log (per-field; @uri is percent-encoding) echo '%2Fapi%2Fusers%3Fid%3D7' | jq -rR @uri # /api/users?id=7 # encode a value for a query string printf 'a b+c' | jq -sR @uri # a%20b%2Bc
# --data-urlencode encodes the value for the request body (or -G for the query string),
# and leaves every character in place: spaces, &, =, a literal +.
curl -G -s --data-urlencode "q=a b+c&d" --data-urlencode "n=1" \
https://example.com/search
The specs, which one says what
The percent-encoding decision is RFC 3986. Section 2.1 defines the escape as a
percent sign plus two hex digits. Section 2.2 marks the reserved characters —
:/?#[]@!$&'()*+,;= — that are structure rather than data.
Section 2.3 names the unreserved set, A-Z a-z 0-9 - . _ ~, the only
characters guaranteed safe in every position; anything outside it should be escaped.
That is the set this page's strict toggle enforces, against
encodeURIComponent's slightly wider one. The +-for-space
rule is not in RFC 3986 at all — it belongs to the WHATWG URL
Standard's application/x-www-form-urlencoded serializer, which is
precisely why it surprises people: the most consequential rule about query strings
lives in a different document from the one about URLs.
FAQ
Why did my plus sign become a space — and why is it sometimes correct?
Two encodings, both right, for different jobs. Form data writes a space as
+, so a literal + has to be %2B to survive.
decodeURIComponent never does any of that: a + stays a
plus. Measured: URLSearchParams reads k=a+b as
a b, decodeURIComponent("a+b") gives a+b.
Same input, two answers, both correct — and this page resolves the ambiguity
by asking which mode you meant, with the answer visible in the notes strip.
What is the difference between encodeURI and encodeURIComponent?
encodeURI encodes a whole URL and keeps
:/?#&=+@ because they are syntax. encodeURIComponent
encodes one value and escapes everything except the unreserved set. The bug is
full-URI encoding a value: "a=b&c" full-URI-encodes to
a=b&c, and the far side reads two parameters. If a string is a
value, component-encode it. The mode control above says which you are doing.
Why do links arrive with %2520 instead of a space?
Double encoding. %2520 is the encoding of %20, which is
the encoding of a space; something upstream encoded an already-encoded string, the
classic tracking-link build bug. This page detects it when a decoded
% is followed by two hex digits, notes it, and the second decode pass
is one step away.
Why does encodeURIComponent leave ( and ) alone, and when does that bite?
encodeURIComponent follows a late WHATWG reading that keeps the
sub-delims ! ' ( ) * out of the escape set after RFC 3986 §2.3, which
lists only the unreserved set. Legal in a query string, so rarely a problem in
transit — the bite is contextual: an unescaped ) ends an
auto-linked URL in chat, an unescaped ' breaks a shell one-liner. The
strict RFC 3986 toggle escapes all five.
What does URIError: URI malformed actually mean?
Six faults, one message. A truncated %, a single hex digit, non-hex
text, a UTF-8 sequence cut short, an impossible lead byte, a bad continuation, an
overlong encoding — all throw URIError: URI malformed.
decodeURIComponent cannot tell you which, because it cannot tell you
anything. This page runs its own scan and reports the fault, the run, and the
character offset, which turns a bisection of your input into one look.
Should a space be %20 or +?
It depends on the mode, and this page's whole thesis is that "it depends"
is the correct answer to give. Path and segment: %20. Form data: a
space is + and a literal + is %2B. Guessing
is how the "why did my URL break" genre happens at scale.