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

How to Fix Double-Encoded URL Parameters

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

October 28, 20267 min read5 topics
How to Fix Double-Encoded URL Parameters
Explore the details
#URL encoding#API debugging#HTTP#JavaScript#query parameters
October 28, 20267 min read

A query parameter that arrives as a%2Bb%20%26%20c instead of a+b & c has usually been encoded more than once—or decoded at a different layer than expected. The fix is to find where the extra transformation happens, then encode the original value once for the query-string builder that owns the request.

This guide shows how percent-encoding changes a value, how to trace it through an API request, and how to inspect the raw request target separately from the server's parsed parameter.

Key Takeaways

  • Pass raw parameter values to a query-string builder; do not pre-encode them and then pass them to another builder.
  • Inspect both the raw request target and the parsed parameter. They show different stages of processing.
  • A value containing %25 where you expected % often indicates an additional encoding pass, but confirm the value at each layer before changing code.
  • Avoid repeatedly decoding input as a general fix. Decode according to the relevant protocol and application contract.

What double encoding looks like

Percent-encoding represents bytes in a URL component with a percent sign followed by two hexadecimal digits. For example, & becomes %26 when it is data inside a query parameter rather than a separator between parameters.

Consider the value a+b & c:

StageValue
Original parameter valuea+b & c
Encoded once for a query parametera%2Bb%20%26%20c
Encoded again as a parameter valuea%252Bb%2520%2526%2520c

The second encoding turns each % into %25. A query parser that decodes the final request once therefore returns a%2Bb%20%26%20c, not the original value. That is a useful clue, but not proof by itself: a literal percent-encoded string may be the intended data.

Encoding is specific to a URL component. This example concerns a query parameter value; a path segment has different delimiters and handling. Also, form-style query serializers commonly represent spaces as +. In that format, a literal plus sign must be escaped as %2B. Do not interpret every + as a literal plus without knowing which parser and serialization rules apply.

Reproduce the extra encoding in JavaScript

In browsers and modern JavaScript runtimes, URLSearchParams serializes parameter values. Give it the original value:

const value = "a+b & c";

const correct = new URLSearchParams({ q: value });
console.log(correct.toString());
// q=a%2Bb+%26+c

The + characters in the serialized output represent spaces under form-style query serialization; the original literal plus is %2B.

This creates the extra encoding pass:

const value = "a+b & c";
const alreadyEncoded = encodeURIComponent(value);

const incorrect = new URLSearchParams({ q: alreadyEncoded });
console.log(incorrect.toString());
// q=a%252Bb%2520%2526%2520c

encodeURIComponent produced a URL-encoded string, and URLSearchParams then treated that string as ordinary data and encoded its percent signs. The usual correction is to pass value directly to URLSearchParams, not alreadyEncoded.

Trace encoding across the request path

Do not assume the bug belongs to the client or server before comparing what each layer receives. A typical path looks like this:

  1. Application code: Starts with a raw value, or receives a value that might already be encoded.
  2. Query builder or HTTP library: Serializes parameter values when it builds the request URL. Passing a pre-encoded value as a parameter can add another encoding pass.
  3. HTTP intermediaries: A proxy, gateway, or framework may log, parse, normalize, or forward the request. The exact behavior depends on the component and configuration.
  4. Server request parser: Converts the query string into parameter values, usually applying the parser's query-decoding rules.
  5. Downstream API client: May encode values again if the server passes an encoded string instead of the original parameter value.

Write down the value at each boundary. The first point where it changes unexpectedly is where to investigate. Do not add a decoder at the end just because the final value looks encoded: that can hide the actual extra encoding and create inconsistent behavior for other inputs.

Compare a raw request target with a parsed value

For a request target containing a double-encoded value:

/search?q=a%252Bb%2520%2526%2520c

A server-side query parser will typically expose the once-decoded value:

a%2Bb%20%26%20c

For example, this JavaScript snippet parses a request target and reads the parameter:

const requestTarget = "/search?q=a%252Bb%2520%2526%2520c";
const parsedUrl = new URL(requestTarget, "http://localhost");

console.log(requestTarget);
// /search?q=a%252Bb%2520%2526%2520c

console.log(parsedUrl.searchParams.get("q"));
// a%2Bb%20%26%20c

In a real server, inspect the raw request target using the server's request interface or a diagnostic log, then inspect the parsed parameter separately. The exact property for the raw target depends on the server and framework. A reverse proxy's access log may show a different representation than the application receives, so compare the closest available points on both sides of a proxy.

Avoid logging sensitive query values in production. If a request may contain credentials, tokens, or personal data, use a controlled test value or redact the value while retaining enough structure to identify %25, %2B, and other relevant escapes.

Send controlled test requests

Use a harmless sample value and compare a correctly serialized request with a deliberately double-encoded one. For example, these request targets illustrate the difference:

/search?q=a%2Bb%20%26%20c
/search?q=a%252Bb%2520%2526%2520c

The first represents the value encoded once; the second represents an encoded string whose percent signs were encoded again. Test against an endpoint you control, and check both the request target and the parsed parameter. If an HTTP client accepts a params or equivalent argument, pass it the raw value and let that client build the query string. If you construct the full URL yourself, ensure the value is encoded once for that specific query component and is not subsequently handed to another parameter serializer.

Fix the layer that adds the extra pass

Once you find the first unexpected change, correct that boundary:

  • Client receives raw data: Pass it as a raw value to the query builder. Do not call an encoder first if the builder will encode it.
  • Client receives an encoded representation by contract: Clarify whether the contract expects a literal encoded string or the original data. Avoid silently mixing the two forms.
  • Server forwards a parameter: Use the parsed value as data when constructing the downstream request. Let the downstream request builder serialize it once; do not encode it first unless its API explicitly requires a pre-encoded component.
  • Server needs the original query text: Preserve or inspect the raw request target through the server's documented interface rather than trying to reconstruct it from parsed values. Parsing and re-serialization can change formatting, such as using + instead of %20 for spaces.
  • Only some inputs fail: Compare inputs containing %, +, &, spaces, and non-ASCII characters. These reveal whether the problem is an extra pass, a form-versus-URI interpretation mismatch, or incorrect handling of delimiters.

Do not fix the symptom by decoding every value until it “looks right.” A legitimate value can itself contain text such as %2F; another decode could change that data into / and alter its meaning.

Common Mistakes

  • Pre-encoding before using a query builder. The builder treats the percent signs as data and escapes them, producing %25 sequences.
  • Calling decodeURIComponent repeatedly. Repeated decoding can change literal data, break delimiter boundaries, or make validation disagree with later processing.
  • Confusing a raw URL with a parsed parameter. A server parser may have already decoded the value once; reading the parsed value is not the same as inspecting the request target.
  • Treating + as unambiguous. In form-style query parsing, + commonly means a space, while %2B represents a literal plus. Check the serializer and parser used by the actual stack.
  • Blaming a proxy without comparing both sides. Capture the request target before and after the intermediary where possible; do not infer its behavior from the final application value alone.
  • Logging full production query strings while debugging. Query strings can contain secrets. Use test inputs or redact sensitive values.

Best Practices

  1. Keep values in their raw, application-level form internally. Serialize them only when building the URL component that will carry them.
  2. Prefer the HTTP client's documented parameter-building API over manual concatenation of query strings.
  3. Record the raw request target and parsed parameter as separate observations in a controlled reproduction.
  4. Test reserved characters, spaces, literal percent signs, plus signs, and Unicode—not just alphanumeric values.
  5. Define whether an API field represents original data or an already-encoded string. Make the contract consistent between callers and servers.
  6. Treat parsing and validation as separate concerns. Correct encoding does not make untrusted input safe for every downstream use.

Documentation limitation: Live language-library documentation could not be retrieved while preparing this article. The JavaScript example illustrates standard URLSearchParams behavior; verify the corresponding request-building and raw-target APIs against the versions and framework used by your application.

Conclusion

To fix double-encoded URL parameters, compare the original value, the raw request target, and the parsed parameter until you find the extra encoding pass. Then pass raw values to the component responsible for query serialization, and avoid decoding arbitrary input as a substitute for tracing the request.