Why Your URL Parameters Are Encoded Twice (and How to Decode Them)
%253A is not a typo — it is what happens when URLs ride inside URLs. How the layers pile up, and how to peel them off safely.
The fragment that looks like garbage but isn't
You open a redirect link and somewhere in the middle sits a stretch like %253A%252F%252F. It looks like corruption. It is not: %25 is the encoding of the percent sign itself, so %253A decodes once to %3A, and %3A decodes once more to a plain colon. This URL has been encoded two layers deep — and that happens every time one URL has to travel inside another URL as a parameter.
- A login service needs to send you back: your callback address becomes a parameter value inside the service's URL.
- That entire service URL is then passed along as a parameter of a third URL.
- Every hop wraps the previous layer once more, and every wrapping doubles each percent sign.
After three hops a single colon has grown into %25253A. Nothing is broken — there is simply one layer per hop.
Why the layers pile up
Each layer is added by a different piece of software doing exactly the right thing:
- Your app builds https://app.example.com/callback?next=/settings?tab=security, encodes it once with encodeURIComponent before inserting it, and the parameter reads next=%2Fsettings%3Ftab%3Dsecurity.
- The SSO service, when forwarding you onward, treats the entire service URL — parameters and all — as ordinary text and encodes it again, which is where the %3A%2F%2F sequences inside its own URL come from.
- Any further hop (an API gateway, an email click-through, a tracking redirect) repeats the process.
Nobody in the chain sees more than their own layer, so the depth tells you how many hops a URL has been through — not that somebody made a mistake.
Three rules that make nested URLs manageable
- Decode repeatedly until nothing changes. One pass over %253A yields %3A — still encoded. Decoding is only complete when a pass stops producing changes, and the number of passes is the depth.
- Watch the plus sign. In query values, a + has meant a space since the earliest web forms, while %20 is the modern, unambiguous spelling. A good decoder treats the two as the same character inside a query string — and writes the + back when rebuilding, so the URL still round-trips exactly.
- Encoding is not unique, but it is equivalent. A colon may appear raw or as %3A depending on which encoder produced it; both mean the same thing to a conforming server. When comparing URLs, expect semantic equality, not byte equality.
The traps worth knowing
- Fixing things into double-encoding. If a value already contains %2F and you run it through encodeURIComponent again "to be safe", you get %252F — the server now decodes that to the literal text %2F instead of a slash. Encode raw text only, never text that is already partly encoded.
- Using encodeURI on parameter values. encodeURI deliberately preserves ?, &, = and #, because it is meant for a complete URL. Inside a parameter value those characters must be encoded, or they break the structure of the outer URL. Use encodeURIComponent — component encoding — for values.
- Editing after decoding only one layer. You change tab=security to tab=privacy in the middle layer, copy the whole URL, and leave — but every outer layer still wraps the old value. Edit at the innermost level, then re-encode outward, layer by layer, exactly the way it was built.
Decode one in your browser
The URL Encoder / Decoder tool on this site untangles the whole chain locally in your browser: paste a URL and it lists every parameter, expands nested URLs into editable tables, highlights which characters belong to which decoding layer, and rebuilds the outer URL as you type — re-applying each layer's encoding automatically. A quick encoder on the same page applies one to five layers when you are building such links yourself. Nothing is uploaded anywhere.