Double URL Encoding (%2520): Causes and Fixes

Updated

%2520 is a space that has been URL-encoded twice. The first encoding turns the space into %20; a second encoding treats the % as an ordinary character and turns it into %25, which leaves %2520. The fix is to find the step in your code, CMS or redirect rules that encodes a string that was already encoded, and remove it.

RFC 3986 states the rule directly: implementations must not percent-encode or decode the same string more than once, because a decoded string can be mistaken for an encoded one and the reverse. Double encoding is what happens when two layers each think they own that job.

How a space becomes %2520 A space is encoded once to %20. Encoding again turns the percent sign into %25, giving %2520. One decode of %2520 returns %20, not a space. Tip: %25 followed by two hex digits signals double encoding. How a space becomes %2520 "annual report" raw text annual%20report encoded once (correct) annual%2520report encoded twice (bug) space → %20 % → %25 Decode %2520 once → %20 The browser asks the server for a path containing the characters "%20", not a space. Spot it: look for %25 followed by two hex digits %2520 = space %252F = / %2526 = & %25C3%25A9 = é Fix the step that encodes an already encoded string.
How a space becomes %20 and then %2520 after a second encoding pass

How to recognize double encoding

The signature is %25 followed by two hexadecimal digits. %25 is the encoded percent sign, so %25XX almost always means an encoded escape sequence.

What you see Decoded once Decoded twice Original
%2520 %20 space space
%252F %2F / slash
%2526 %26 & ampersand
%25C3%25A9 %C3%A9 é é
%25252520 %252520 %2520 encoded three times

A quick regular expression flags candidates in logs, crawls or exported link lists:

const doubleEncoded = /%25[0-9A-Fa-f]{2}/;
doubleEncoded.test("/files/annual%2520report.pdf"); // true
doubleEncoded.test("/files/annual%20report.pdf");   // false

Treat a match as a lead, not proof. A value such as discount=100%25 is a correct single encoding of "100%"; it only becomes suspect when the %25 is followed by two hex digits.

Where the second encoding usually comes from

1. Encoding a whole URL that is already encoded

Code that receives a complete URL and runs it through encodeURIComponent() again is the most common source.

const target = "https://example.com/search?q=a%20b";
encodeURIComponent(target);
// "https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Da%2520b"

That result is correct if you are putting the whole URL inside another URL's query parameter, for example ?redirect=.... It is wrong if you then use it as the link itself.

2. Encoding values twice in a pipeline

encodeURIComponent(encodeURIComponent("a b"));   // "a%2520b"
encodeURIComponent(encodeURIComponent("é"));     // "%25C3%25A9"

This often happens when a helper encodes values and the caller has already encoded them, or when URLSearchParams (which encodes for you) is fed a value that was pre-encoded:

const params = new URLSearchParams();
params.set("q", encodeURIComponent("a b")); // value is "a%20b"
params.toString();                           // "q=a%2520b"

3. CMS slugs and templates

A CMS stores a slug already percent-encoded, and the theme template encodes it again when it prints the link. The page itself works under the single-encoded URL, but every internal link points to the double-encoded one.

Server rewrite rules, redirect plugins and URL shorteners may escape the target URL themselves. If the rule's target is written with %20 and the software escapes it again, visitors land on %2520. Campaign link builders do the same when you paste a URL that already contains encoded UTM values. See UTM parameter encoding mistakes for the analytics side of this problem.

5. Spreadsheet exports

Running Excel or Google Sheets ENCODEURL on a column that already contains encoded URLs produces %25 sequences in bulk. The Sheets and Excel encoding guide shows how to check a column before you encode it.

A fix checklist

  1. Collect examples. Export the URLs that contain %25 from your crawler, server logs or Search Console.
  2. Decode one level and compare with the URL you intended. If one decode gives the correct URL, you have exactly one extra encoding step.
  3. Trace the link to its source: template, plugin, redirect rule, sitemap generator, feed or ad platform.
  4. Remove the extra step. Encode raw values once, at the moment you build the URL. Do not encode strings you received as URLs.
  5. Redirect existing double-encoded URLs to the correct ones with a single 301 while old links still exist.
  6. Update sitemaps and canonical tags so they list only the single-encoded form.

If the double-encoded URLs pass through a redirect chain, a redirect error check helps confirm that each old address now ends on the right page in one hop.

Repairing data you already have

For a one-off clean-up of stored links, you can decode until the string stops changing, with a limit and error handling:

function decodeRepeated(s, maxRounds = 3) {
  for (let i = 0; i < maxRounds; i++) {
    let next;
    try { next = decodeURIComponent(s); } catch { break; } // malformed % sequence
    if (next === s) break;
    s = next;
  }
  return s;
}
decodeRepeated("annual%2520report"); // "annual report"

Use this carefully. It fully decodes the string, so re-encode it once before using it as a URL. It also destroys legitimate encoded percent signs: 100%2525 meant 100%25 and ends up as 100%. That is why fixing the source is better than cleaning the output.

How to prevent it in your own code

A few habits stop double encoding before it starts:

  • Encode values, not URLs. Apply encodeURIComponent, rawurlencode or quote to each parameter value when you build the URL, and never to a URL you received from somewhere else.
  • Use one builder. Let URL and URLSearchParams (or your framework's URL helper) assemble the whole address. Do not pre-encode values you pass to them; they encode for you.
  • Store raw values. Keep slugs, campaign names and search terms unencoded in the database and encode at output time. A stored value that is already encoded is the most common source of a second pass.
  • Decode at the edge, once. Most frameworks decode query parameters before your code sees them. Calling decodeURIComponent again on $_GET or req.query values decodes twice, the mirror image of this bug.
  • Test with a space and a percent sign. A value such as 50% off sale exercises both cases: it should appear as 50%25%20off%20sale (or with + for spaces in form encoding), never with %2520 or %2525.

Double encoding vs other encoding problems

Symptom Likely cause Covered in
%2520, %252F Encoded twice This guide
+ arrives as + instead of a space Form encoding read with plain percent-decoding %20 vs + in URLs
amp; appears in parameter names HTML entity &amp; copied into a raw URL HTML entities vs URL encoding
é or &#xE9; instead of é A different escape format was used Unicode escape converter

For how encoded URLs are crawled and indexed in general, see URL encoding and Google indexing.

Check a URL now

Paste a suspicious address into the URL parser to see each query parameter decoded once. If a value still contains %20 after decoding, the URL was encoded twice. For single strings, the URL decode operation in the universal encoder decoder lets you decode step by step and watch each layer come off.

Frequently asked questions

What does %2520 mean in a URL?

It is a space that was percent-encoded twice. The first pass turns the space into %20. The second pass encodes the percent sign itself as %25, so %20 becomes %2520. Decoding it once gives %20, not a space.

Is %2520 bad for SEO?

It usually creates a second URL that does not match the page you meant. The double-encoded link may return a 404, redirect, or show the same content under a different address. Fix the link source so crawlers only see the single-encoded form.

Can I just decode the URL twice to fix it?

Only as a short-term repair for data you already have. Repeated decoding breaks values that legitimately contain a percent sign, such as 100%25 meaning 100%. The real fix is to find the step that encodes an already encoded string and remove that step.

Split a URL into scheme, host, path, query and fragment, and decode every parameter.

Open the URL Parser

Sources