How to Decode a Double-Encoded URL, One Layer at a Time
A string that still contains %XX after decoding may have another percent-encoding layer—or it may be literal data. Inspect the result before decoding again.
URL decoding has two separate jobs that are easy to mix up: turning %XX triplets back into bytes, then interpreting those bytes with the correct character encoding. A link can be percent-encoded more than once, and text from an older Japanese site may use Shift_JIS rather than UTF-8. Each step has to match how the value was created.
See the order at a glance
A verified Shift_JIS example
This example uses the text リP. Its Shift_JIS bytes are 83 8A 50. Percent-encoding those bytes once, then encoding the percent signs for a second URL layer, produces this string:
%2583%258A%2550%83%8A%5083 8A 50 → リPI reproduced this round trip locally with Python’s standard urllib.parse and shift_jis codec. The character result was checked as Unicode code points U+30EA U+0050, matching リP. This proves this constructed example—not the encoding of any particular third-party website.
For your own string, keep a copy of the original. Remove one percent layer and inspect the output. If it now contains valid %XX byte escapes, identify the source charset from the application or site that created it. Only then convert those bytes using that charset.
In Python, the same two operations can be kept explicit so you can inspect the bytes between them:
from urllib.parse import unquote, unquote_to_bytes
encoded_twice = "%2583%258A%2550"
one_layer = unquote(encoded_twice) # "%83%8A%50"
raw_bytes = unquote_to_bytes(one_layer) # bytes 83 8A 50
text = raw_bytes.decode("shift_jis") # "リP"
Use this only when the source really used Shift_JIS. If the source charset is unknown, the correct decoded characters are unknown too.
How to tell whether another decode is justified
%25 became new %XX triplets.Common traps
| What you see | What it may mean | Safer next move |
|---|---|---|
%25 changes to % | A percent sign from another encoded layer was exposed. | Inspect the full intermediate string; do not decode again automatically. |
Valid %XX groups remain, but text is garbled | The bytes may use a different character set than UTF-8. | Confirm the source charset before converting the bytes. |
%2F, %3F, or %26 appears | Decoding may reveal a slash, question mark, or ampersand that changes URL structure. | Decode a component separately; do not alter a full URL’s delimiters unintentionally. |
| A percent sign is followed by non-hex text | The percent may be literal text or the string may be malformed. | Keep the source and handle it as text; do not force a second decode. |
Decode a URL value without changing the rest of the link
If you only need to inspect a normal UTF-8 URL value, open the PercentURL URL Decoder. For a full URL, use URL Encode and Decode and choose the relevant mode. These browser tools do not infer an unknown legacy character set; use the source application’s charset information for that step.
Common questions
Why does my URL still contain percent signs after decoding?
Those signs may be part of another percent-encoding layer, or they may be literal data. Decode one layer and inspect the result before deciding what it means.
Can UTF-8 decode every percent-encoded string?
No. Percent triplets represent bytes, and those bytes need a character encoding. Modern web examples often use UTF-8, while some older data may use another charset. Use evidence from the system that produced the value.
Should I keep clicking decode until I can read it?
No. Each decode changes the string and may expose URL delimiters. Preserve the original and stop when you cannot identify the next layer or charset.
References
- RFC 3986 §2.1 — Percent-Encoding and §2.4 — When to Encode or Decode.
- MDN: decodeURIComponent() — decoding a URI component and malformed UTF-8 behavior.
- WHATWG Encoding Standard: Shift_JIS — the web platform’s Shift_JIS decoding algorithm.