TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

Why URL Encoding Breaks When You Encode a Query String Twice

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20266 min read5 topics
Why URL Encoding Breaks When You Encode a Query String Twice
Explore the details
#JavaScript#URL encoding#URLSearchParams#Web development#Troubleshooting
October 26, 20266 min read

Encoding a query value twice can turn %2F into %252F, leave %2F in the value after one decode, or cause different layers of an application to interpret the same input differently. The fix is usually to encode each value once at the boundary where it becomes a URL component—not to encode or decode the complete URL repeatedly.

Key Takeaways

  • Use encodeURIComponent() for an individual value when building a URL manually; use URLSearchParams for query parameters in JavaScript.
  • encodeURI() is intended for a mostly assembled URI. It preserves characters such as &, =, and ?, so it is not a substitute for encoding a query value.
  • Double encoding commonly occurs when already-encoded text is passed through another encoding step: % becomes %25.
  • Parse and serialize URLs with URL-aware APIs, and agree on which layer decodes each value.
  • Do not repeatedly decode untrusted input or treat a URL parser as a security validator.

What double URL encoding looks like

Percent-encoding represents a byte using % followed by two hexadecimal digits. For example, & can be represented as %26. If you encode that already-encoded text again, the percent sign is encoded as %25:

const once = encodeURIComponent("a&b");
const twice = encodeURIComponent(once);

console.log(once);  // a%26b
console.log(twice); // a%2526b

After one decode, a%2526b becomes a%26b, not a&b. A second decode changes it again. That difference matters when one part of an application encodes a value and another part assumes the value is still raw.

Choose the right API for the job

encodeURIComponent() encodes a component

Use encodeURIComponent() when you need to encode one query parameter value yourself. It encodes separators such as & and =, so they stay part of the value instead of becoming query syntax.

const value = "red & blue";
const query = `color=${encodeURIComponent(value)}`;

console.log(query); // color=red%20%26%20blue

This function encodes a URI component, not necessarily every character that could be excluded by a particular application’s stricter rules. If you need a specific canonical format, define and apply that format consistently rather than adding another encoding pass.

encodeURI() preserves URL syntax

encodeURI() is for a URI that is already assembled. It leaves structural characters—including ?, &, and =—unescaped so the URL’s delimiters remain meaningful.

const url = "https://example.com/search?q=red blue&lang=en";
console.log(encodeURI(url));
// https://example.com/search?q=red%20blue&lang=en

That behavior makes encodeURI() the wrong choice for an individual query value containing & or =: those characters can be interpreted as query delimiters. Conversely, applying encodeURIComponent() to a whole URL encodes its structural punctuation, so the result is text representing a URL rather than a usable URL with the same structure.

Prefer URL and URLSearchParams for query strings

For ordinary web URLs, let the platform construct and serialize the query string. Pass the raw value to URLSearchParams; do not pre-encode it.

const url = new URL("https://example.com/search");
url.searchParams.set("q", "red & blue");

console.log(url.href);
// https://example.com/search?q=red+%26+blue
console.log(url.searchParams.get("q"));
// red & blue

The serialized space appears as + in this query format, while the ampersand within the value is percent-encoded. Reading the parameter returns the decoded value. If you pass an already encoded string instead, the percent sign is encoded too:

const url = new URL("https://example.com/search");
url.searchParams.set("q", "red%20%26%20blue");

console.log(url.searchParams.get("q"));
// red%20%26%20blue

That result is not a second decode; it is the literal value you supplied. The serialized URL will protect its percent signs as part of that value. Supply the raw text if you want the parameter to represent red & blue.

The examples have not been executed in a JavaScript runtime here; the displayed results follow the documented API behavior. Check the exact serialization your application relies on in its target runtime and test the full client-to-server path.

A practical debugging workflow

  1. Find every transformation. Search the client, server, framework middleware, and redirect or proxy code for encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, URLSearchParams, and framework-specific query parsing.
  2. Inspect the value at each boundary. Record both the serialized URL and the value returned by the parser. For example, distinguish q=a%2526b in the URL from a%26b after one decoding step.
  3. Identify who owns encoding. If URLSearchParams serializes the query, give it raw parameter values. If manually constructing a query, encode each value once with encodeURIComponent() and preserve the separators you add yourself.
  4. Check the server’s parsing contract. Many frameworks parse and decode query parameters before application code sees them. Applying decodeURIComponent() again can turn data into delimiters or change the value unexpectedly.
  5. Add a round-trip test. Include spaces, %, &, +, =, #, and non-ASCII text. Assert the parsed value, not just the appearance of the serialized URL.

For example, a literal percent sequence and a space are different inputs. With URLSearchParams, setting q to %2F means the value contains the three characters %, 2, and F; setting it to / means the value contains a slash. Test both cases so a future encoding change does not silently conflate them.

Decoding: do it once, at the right layer

A decoded value may contain characters that look like URL syntax. For instance, decoding %26 produces &. That is expected when & is data inside a parameter, but it can cause problems if later code reparses the decoded value as query syntax.

Do not repeatedly decode until a string “looks normal.” Repeated decoding can expose delimiters that an earlier layer treated as data. Also, decodeURIComponent() throws a URIError for malformed percent escapes; do not assume arbitrary input is valid. Define one parsing and decoding boundary, handle malformed input there, and validate the resulting value according to its intended use.

If a parameter is supposed to contain a complete URL, treat that URL as one parameter value and encode it once through URLSearchParams or encode the component once with encodeURIComponent(). Parse the value as a URL only after retrieving it, and apply the application’s own scheme, host, and other validation rules. Parsing alone does not establish that a destination is safe.

Common Mistakes

  • Encoding the whole query string as one value. This can encode the & and = separators that are supposed to divide parameters.
  • Pre-encoding values passed to URLSearchParams. The API serializes parameter values; give it the raw value, not the result of encodeURIComponent().
  • Using encodeURI() for a parameter value. Reserved delimiters can remain unescaped and change the query structure.
  • Decoding twice because the first result still contains %. A percent sequence may be literal data, or it may be intended for a later, explicitly defined decoding layer. Do not guess.
  • Assuming all parsers serialize identically. Query serialization has rules for spaces and other characters; compare parsed values and account for the parser used at each boundary.

Best Practices

  • Choose a single owner for query serialization: usually URLSearchParams in JavaScript.
  • Keep values raw within application code and encode only when serializing them into a URL component.
  • Document whether an API expects a raw value or an already serialized URL. Avoid ambiguous interfaces that accept either.
  • Test the exact string received by the server and the value exposed by its query parser.
  • Use a URL encoder and decoder to inspect an example while debugging, but verify behavior in your application’s actual parser. An online tool does not replace application-specific validation.

Conclusion

Double encoding usually means an already encoded value passed through another encoding step. Use URLSearchParams with raw values for query construction, or encode a single component once when building it manually. Then decode at one defined boundary and test the value through the complete client-server path.

References