Skip to content
Tresna

URL Encoder & Decoder

Encode and decode percent-escapes with the right scheme. Shows what stays unescaped in each, handles UTF-8 correctly, and flags anything it could not read exactly.

encode

RFC 3986. Escapes everything except letters, digits, - . _ ~. The safe default.

strict2152
Hello%2C%20world%21%20Caf%C3%A9%20%E6%97%A5%E6%9C%AC

Encoded: Hello%2C%20world%21%20Caf%C3%A9%20%E6%97%A5%E6%9C%AC

Runs in this browser tab. Nothing is uploaded, and the text is not stored or logged.

How to use this tool

A URL can only carry a restricted set of characters safely. Anything else — a space, a slash, a `?`, a non-ASCII letter — has to be written as `%` followed by two hex digits, which is what percent-encoding is. The awkward part is that there is more than one rule for deciding which characters need it, and picking the wrong one produces a URL that looks fine and fails.

  1. 1`encode` turns text into escapes, `decode` reads them back, and `query` reads a whole query string into a table.
  2. 2In `encode`, choose the scheme by where the value is going: a path segment, a query value, a whole URL, or a form body.
  3. 3In `decode`, tell it whether a `+` is a space. It usually is — and when it is not, nothing in the text will tell you.

There are four rules, not one

JavaScript ships two encoders and a form parser, and they do not agree. This page offers all four schemes explicitly, because choosing between them from memory is how URLs break.

SchemeLeaves unescapedUse it for
strictA–Z a–z 0–9 - . _ ~Anything, when you are unsure. The modern default.
component…plus ! * ' ( )Matching encodeURIComponent exactly. A query value or a path segment.
full…plus / ? : @ & = + $ # ;A whole URL, so its structure stays readable.
formA–Z a–z 0–9 * - . _application/x-www-form-urlencoded. A space becomes +.

The one to watch is `component`, because it is what most people reach for. It leaves `!'()*` unescaped, and those are legal in a URI *component* but not in a URI *identifier* — so a server that reads RFC 3986 strictly will reject a request that looks completely fine in the browser bar.

The + that is sometimes a space

A space can be written two ways: `%20`, which works anywhere, or `+`, which only works in an `application/x-www-form-urlencoded` context. A query string is one, so `?q=hello+world` means `hello world`. A path segment is not, so `/a+b` in a path is a literal plus sign.

The problem is that nothing in the string says which context you are in. A decoder has to be told, and this page defaults to treating `+` as a space — because a query string is the more common paste — with a checkbox for the other case.

# a query string
curl 'https://api.example.com/search?q=hello+world'   # "hello world"

# a path segment - the + is a plus
curl 'https://api.example.com/files/hello+world'    # "hello+world"
The same string, read two ways.

UTF-8, and the order that matters

A character like `é` is one character in JavaScript and two bytes in UTF-8, and each byte gets its own escape. The order matters: encode to UTF-8 first, then escape each byte. Escaping the character directly gives `%E9`, which is Latin-1, and every server expecting UTF-8 misreads it.

"é"  ->  %C3%A9   correct: UTF-8 bytes, one escape each
          ->  %E9      wrong: the code point escaped as if it were a byte

"🎉"  ->  %F0%9F%8E%89   four bytes, because it is a surrogate pair in UTF-16
One character, two escapes, and why the wrong order is a different answer.

A four-byte character is where hand-rolled encoders break. A surrogate pair is two code units in JavaScript, so encoding unit by unit gives you six bytes where the character is four — or a replacement character if something trims the invalid half.

Don't encode twice

Encoding an already-encoded string turns `%20` into `%2520`, and there is no way to tell from the result that it happened. The value still looks like a URL, which is what makes it hard to spot. If something is already encoded, decode it first and see whether it changes.

a b            ->  a%20b        or  a+b in a form body
a%20b          ->  a%2520b      now it decodes to "a%20b", not "a b"

If decoding a value twice gives you something different the first time did,
you have found the double encoding.
The same value, encoded once and then twice.

Reading a query string

The `query` mode shows parameters as a table rather than handing you a joined string, which is usually what you want when you are debugging a request. Two things it deliberately does not do:

  • It keeps repeated names as separate rows. `?tag=a&tag=b` is two parameters, and `URLSearchParams.toString()` will hand you `tag=a%2Cb` — one parameter with a comma in it, which is a different request.
  • It keeps a name with an empty value. `?c=` is a present parameter with no value, and dropping it loses the fact that it was sent.

When this page says it could not read something

A `%` starts an escape, and the two characters after it have to be hex digits. `%ZZ` is not a character, and `%E9` on its own is not valid UTF-8 — it is a Latin-1 `é` that a UTF-8 reader cannot finish.

Rather than drop those or turn them into `�`, which is indistinguishable from a real character, this page keeps what it can and tells you what it could not read. You get the best available answer and the warning that it is not exact — because a mangled string pasted into a request is a much harder afternoon than a labelled one.

100% sure     a stray %. Kept as "%", and flagged.
caf%E9        a Latin-1 é. Kept as a replacement, and flagged.
a=%2         a truncated escape. Kept, and flagged.
Three inputs that are not what they look like.

Doing it yourself

For anything but a whole-URL rewrite, the built-ins are enough and a library is more dependency than the job needs. The one case worth reaching for a library is building a query string from structured data, where the escaping of the *names* is easy to forget.

encodeURIComponent("hello world")     // "hello%20world"
decodeURIComponent("hello%20world")     // "hello world"

// a query string, names included - the part people forget
const q = new URLSearchParams({ q: "a b", "a key": "v" }).toString()
// "q=a+b&a+key=v"

// a whole URL, structure intact
encodeURI("https://example.com/a b?c=d")  // "https://example.com/a%20b?c=d"
The three lines that cover most cases.

Frequently asked questions

What is the difference between encodeURI and encodeURIComponent?
encodeURI leaves a URL's structure alone — the slashes, question marks, ampersands and hash — so it is for a whole URL. encodeURIComponent escapes them, so it is for one piece of it: a path segment or a query value. Using encodeURIComponent on a whole URL turns every separator into %2F and you get one long path.
When should I use strict rather than component?
Strict, unless you have a reason. `component` matches encodeURIComponent, which leaves ! ' ( ) unescaped. That is legal in a URI component but not in an identifier, and a server reading RFC 3986 strictly will reject it. Strict has no such edge case.
Is a + a space?
In a query string, usually. In a path segment it is a literal plus sign. Nothing in the text distinguishes them, so the decoder here asks, defaulting to space because a query string is the more common paste.
Why does my encoded value look double-encoded?
It is. Encoding something twice turns %20 into %2520, and the result still looks like a URL. Decode it and see whether the second decode changes it — if it does, the first pass was redundant.
What does the warning about not reading something exactly mean?
The input contained a `%` that was not a valid escape, or bytes that are not valid UTF-8. The text shown is the best available reading, but it is not a faithful decode, so encoding it again will not get you back to what you started with. Decoding `%E9` as a replacement character is exactly the failure this avoids.
Does this send my URL anywhere?
No. Everything happens in your browser tab. Nothing is uploaded, stored or logged — which matters more here than for most tools, since a URL often carries a token or an identifier worth keeping to yourself.