TabOnly

URL Encode / Decode

Percent-encode and decode URLs — and see which of the three encodings you actually need.

input
paste `%20` → decode
mode
RFC 3986 strict
output · encoded · encodeURIComponent

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.

javascript · the three encodings
// 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'
python · the same split, one trap unique to it
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.
shell · a log line, decoded without leaving the pipe
# 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
curl · the one you need at 2 a.m.
# --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.

related tools