Why does my URL show %2520
Tools Nimbus is a free, no-signup developer toolkit that runs entirely in your browser, so your data is never uploaded to a server. A URL showing %2520 is not corrupted: it is a space that was percent-encoded twice, once to %20 and again to %2520. Paste it into the Tools Nimbus URL Encoder and Decoder, free with no signup, and decode once per layer until the value is clean.
Last updated July 2026
The arithmetic behind %2520
Percent-encoding replaces a character with a % followed by its two-digit hex byte value. A space is byte 0x20, so it becomes %20. The catch is that % is itself a reserved character: it is the escape marker, so it cannot appear literally in an encoded string. When a value needs to carry a real percent sign, that sign is encoded as %25, because 0x25 is the byte value of %.
Put those two rules together and double encoding is inevitable. Feed the already-encoded string %20 back into an encoder and it sees three characters: %, 2, and 0. The percent becomes %25, while 2 and 0 are unreserved and pass through untouched. The output is %25 + 20, which is %2520. Nothing malfunctioned. The encoder did exactly its job, on input that had already been through it once.
Counting the layers
Every additional pass inserts one more 25 immediately after the percent sign. That makes the encoding depth readable straight off the string, which is the fastest triage available:
| Raw character | Encoded once | Encoded twice | Encoded three times |
|---|---|---|---|
| space | %20 | %2520 | %252520 |
% | %25 | %2525 | %252525 |
& | %26 | %2526 | %252526 |
= | %3D | %253D | %25253D |
/ | %2F | %252F | %25252F |
Decoding removes one 25 per pass. If you decode a value and the output still contains percent escapes that look like data rather than structure, decode again. When the output stops changing, you have reached the raw value and the number of passes you made is the number of encoders in your chain.
Symptom, cause, and fix at a glance
| Symptom | Most likely cause | Fix |
|---|---|---|
Spaces render as %2520 in the address bar | A value was encoded, stored, then encoded again on render | Store raw values; encode once at URL construction |
%3A%2F%2F where :// belongs | The whole URL was encoded instead of its parts | Encode path segments and parameter values only |
Query separators appear as %26 and %3D | A query string was encoded as if it were one value | Let the query-string builder encode each value |
A nested redirect_uri shows %252F | A URL inside a parameter got encoded twice by design | Decode once at the boundary that reads it |
| Server returns 404 on a path with a space | Proxy or gateway re-encoded an encoded path | Normalize once at the edge, not at every hop |
Literal %2520 saved in the database | Encoded input was persisted instead of the raw value | Decode on ingest; backfill affected rows |
Cause 1: encoding a value that arrived already encoded
This is the overwhelming majority of cases. A parameter comes in over the wire percent-encoded, as it must be. Your framework decodes it for you into the raw value, or it does not, and this is where the confusion starts. Some routers hand you decoded values; some hand you the raw query string. Code that calls encodeURIComponent on a value it did not first confirm was decoded adds a second layer.
The tell is that the bug is invisible for ASCII-only inputs and appears the moment a user types a space, an ampersand, or an accented letter. A value like report survives any number of encoding passes unchanged, because every character in it is unreserved. Only when a reserved character enters the value does an extra layer become visible. That is why these bugs reach production: the happy path never triggers them.
Cause 2: encoding the whole URL instead of its parts
Percent-encoding operates on values, not on URLs. A URL is a structure whose delimiters (:, /, ?, &, =, #) carry meaning. Running an entire assembled URL through encodeURIComponent escapes those delimiters along with everything else, so https://example.com/a b?q=1 becomes a single opaque token beginning https%3A%2F%2F. Any component of it that was already encoded now shows a doubled escape.
// Wrong: escapes the URL's own structure
const bad = encodeURIComponent(`/search?q=${term}`);
// Right: encode each value once, as the URL is built
const url = new URL("/search", "https://example.com");
url.searchParams.set("q", term); // encodes term exactly onceThe URL and URLSearchParams APIs exist to make this hard to get wrong. They take raw values and encode on serialization. Hand them an already-encoded value and you get %2520 right back, so the discipline is to keep raw values raw until the moment of assembly.
Cause 3: a value that legitimately needs two layers
Not every double encoding is a bug. When a URL is carried inside another URL, as with an OAuth redirect_uri or a return_to parameter, the inner URL is a value of the outer one and must be encoded to survive transit. Its own / characters become %2F. If that inner URL itself contained an encoded parameter, that parameter is now at two layers, correctly, and %252F is exactly what should appear on the wire.
The rule that keeps this straight is that each boundary decodes exactly once, and only the layer it owns. The outer service decodes the parameter to recover the inner URL; the inner service decodes that URL's own parameters. Trouble starts when a middlebox decodes greedily and keeps going, or when a handler decodes the value it was given and then passes it on without re-encoding.
Cause 4: proxies, gateways, and framework middleware
A request can pass through a CDN, a reverse proxy, a load balancer, and a framework router before your handler sees it. Each of these may normalize the path. Most decode once and forward the decoded form; a few forward the original and decode again downstream. Two components that each assume they own the decoding step will either double-decode, which is a security concern because it can smuggle an encoded ../ past a path check, or double-encode, which produces the %2520 you are looking at.
Diagnose this by comparing the path at each hop, ideally by logging the raw request target at the edge and again in the application. Once you know which hop introduced the extra layer, disable normalization there rather than compensating with an extra decode downstream. A compensating decode looks like a fix and quietly breaks any value that legitimately contains a percent sign, such as a discount code reading SAVE20%.
Cause 5: encoded values persisted to storage
If %2520 appears in your database, an encoded value was written instead of the raw one, and the render path encoded it a second time on the way out. This is the most expensive variant because the data itself is now wrong and a code fix alone will not repair existing rows. Decode on ingest, store raw text, encode only at output, and backfill the affected rows with a one-pass decode that you have first tested against values containing real percent signs.
Diagnose it in one step
You do not need to reason about the chain to identify the layer count. Open the URL Encoder and Decoder, paste the URL or the single parameter value, and decode. If the output still shows percent escapes standing in for ordinary characters, decode again and count the passes. Two passes to reach clean text means one encoder too many, and the table above tells you which cause to hunt for. Because it runs entirely in your browser using the native encodeURIComponent and decodeURIComponent APIs, a URL carrying a session token or a signed redirect never leaves your device.
If the value turns out to be HTML-escaped rather than percent-encoded, with artefacts like & instead of %2520, the HTML Entity Encoder and Decoder is the right tool, and HTML entities vs URL encoding explains which escaping belongs where. Both mistakes come from the same habit of escaping a value without knowing what state it is already in. Browse the Tools Nimbus guides for more developer fixes.
How to prevent it
- Encode exactly once, at the moment you build the URL. Treat encoding as a serialization step, not something a value carries around with it.
- Store and pass raw values internally. A variable holding
hello%20worldis a bug waiting for the next function that encodes it. - Use
URLandURLSearchParams(or your language's equivalent) rather than string concatenation, and give them raw input. - Never encode an assembled URL. Encode path segments and parameter values individually.
- Decode once per boundary you own, and never add a compensating decode to paper over an upstream double encode. It breaks legitimate percent signs.
- Test with a value containing a space, an ampersand, and a literal
%. Unreserved-only test data hides every one of these bugs. For the wider picture of which escaping applies to which context, see the best free online URL encoder and decoder.