Skip to content
← escentmachine field notes

How to Recognize and Fix Double URL Encoding

On this page
AI-generated illustration; not taken on location · Conceptual encoding ladder showing / becoming %2F and then %252F, alongside labels for intended values.
AI-generated illustration; not taken on location · Successive encoding changes the representation; the intended value determines whether another layer is appropriate.AI-generated illustration

To diagnose double URL encoding, write down the exact value the receiver should obtain, then trace it through serialization and parsing. Repair the first step that introduces an unwanted layer. A readable result alone does not establish that decoding was correct.

The sequence %252F deserves attention because percent-decoding it once produces %2F. Whether that result is wrong depends on the intended data. A receiver expecting / has a different contract from one expecting the literal text %2F. An inner URL carried inside an outer query parameter can also legitimately require another encoding layer.

The investigation therefore needs two kinds of evidence: the expected value at each boundary and the representation actually observed there. The following examples show how to compare them, locate the mismatch, and verify a repair without damaging literal percent text or query structure.

Define the value the receiver should obtain

Begin with an expectation that can be compared character for character. “The URL should work” leaves too much unresolved. “After query parsing, the application should receive a/b” identifies both the value and the checkpoint.

Record the intended text, the field carrying it, and any documented nesting. Establish whether that field contains application text, a path segment, a query value, or an inner URL. Also record whether your observation comes from serialized URL text or from a value already returned by a parser.

Intended value Possible encoded representation What it establishes
Literal 50% 50%25 Encoding the percent character can be correct.
Literal %20 %2520 An apparent extra layer can preserve literal text.
/ Compare the representation at each boundary The expected receiver value determines whether another layer is unwanted.

Do not diagnose the problem from appearance alone. A percent sequence can belong to the intended application data.

Write a separate expectation for each parsing boundary: “At this checkpoint, expect this exact string.” If that string is uncertain, resolve the contract before changing the transformation.

Recognize the clue in %252F

The slash example makes successive component-encoding operations visible:

encodeURIComponent('/');
// Expected: "%2F"

encodeURIComponent(encodeURIComponent('/'));
// Expected: "%252F"

Here, the starting application value is /. If the contract requires one component-encoding step, the second expression adds an unwanted layer. Percent-decoding %252F once produces the text %2F, so that checkpoint has not recovered the intended slash.

Now change the intended receiver value to the literal string %2F. The diagnosis changes with it: a representation containing %252F can be appropriate because one decoding step recovers the required literal text.

Spaces provide a similar comparison. Encoding a space as a component and then encoding that result can produce %2520. But %2520 can also correctly represent an intended value of literal %20.

Ask both questions together: What existed before serialization, and what should exist after the documented decoding step? Treat %252F and %2520 as clues that need a trace. Their presence alone does not establish an error.

Locate the first unexpected transformation

Follow one reproducible input through the available handoffs. Keep the original observations alongside your interpretation so another developer can inspect the same evidence.

Checkpoint Record Question to resolve
Before serialization Exact application value Is this the input the serializer is supposed to receive?
Outgoing URL Captured serialized text Did the expected representation appear?
Intermediary boundaries Observable input and output Where does the first unexpected change occur?
After application parsing Exact value delivered to application code Does it match the expectation for this checkpoint?

Label each representation. Distinguish captured text from a display that may format it for presentation; establish what the display shows before treating a visual difference as a transformation.

For an intended slash, record where %2F first appears and where it becomes %252F. Then determine whether the latter handoff introduces a documented outer layer. An inner URL entering an outer query value may justify that change.

The investigation should end with a specific finding, such as: “This serialization step received encoded component text where its input contract required application text.” That finding identifies both the repair location and the expectation to test.

Match the operation to the URL boundary

Identify whether the operation receives complete URL text, an individual component, or a value for form-style query serialization. Check the selected API’s documented scope before choosing an encoding or decoding operation. Do not infer its suitability from the fact that its output looks URL-like.

Space handling needs an explicit check. Space-to-plus substitution belongs to query conventions or form-urlencoded content. Verify whether the selected operation uses that convention and how the corresponding parser handles a literal plus character.

Test input What to verify
A space Whether the chosen convention uses space-to-plus conversion
Literal + Whether the receiver recovers the intended plus character
Literal & Whether it remains inside the intended value
Literal = Whether the value and parameter boundaries remain correct

For each input, record the serializer input, its output, and the parser result. Compare the operation’s documented purpose with the boundary you are investigating.

Pay particular attention to ampersands. Decoding an encoded ampersand before the intended parsing boundary can change query parameter structure. Successful recovery must preserve both the field’s contents and the boundaries around that field.

Account for intentional nesting

An inner URL carried as an outer query value introduces another boundary. Its representation can legitimately acquire another encoding layer while crossing that boundary.

Record the expected result at each stage:

Stage Expected representation
Before outer serialization The complete intended inner URL text
Inside the outer query A representation carrying that inner text as one value
After outer query parsing The intended inner URL text, including its percent sequences
At the inner parsing boundary The value required by the inner URL’s contract

Suppose the inner URL text contains %2F. The corresponding position in the outer representation can contain %252F. Removing the outer layer produces %2F, which may be exactly the correct result at that checkpoint.

Compare this with accidentally passing encoded component text into a serializer that expects application text. Similar visible sequences can occur in both cases. The documented additional boundary explains the legitimate layer.

Literal percent text also needs an explicit stopping point. If the final intended value is %20, retain that text when the required parsing steps are complete. The disappearance of percent sequences is not a useful success criterion.

Repair the producer's input contract

At the query serialization boundary, supply the raw application value to URLSearchParams.set. For an intended query value of a/b, the supported repair guidance is:

// params is the query parameter collection being prepared.
params.set('q', 'a/b');

The example specifies the input to this boundary. The application supplies the text a/b; the serialization step owns its representation in the query. Write the receiver’s expected value separately as a/b, then verify it through the actual serialization and parsing path.

If your trace shows an earlier encoding operation preparing this same value, inspect why it exists. Remove the unwanted preparation when the query serializer’s contract requires raw application text. For an intentionally nested URL, preserve the complete inner text required at the outer boundary.

Make one step responsible for serialization at each documented boundary. Replace vague requirements such as “accept a URL-safe string” with an explicit description of the accepted representation and expected parser result.

After repairing the producer, review any extra decoding workaround at the receiver. Establish which boundary it serves before changing it, and verify the complete path again.

Handle uncertain or malformed input explicitly

Repeated decoding until the output stops changing gives the investigation no stopping point tied to the intended value. Literal %20, for example, must remain that text after the required decoding boundary has been completed.

Blanket replacement creates a similar problem. Replacing every %25 with % can alter URL delimiters and structure. Legitimate percent data also needs careful treatment: 50%25 can correctly represent the intended value 50%.

When an escape sequence or nesting level is unclear, preserve the original input. Record the received representation, the expected checkpoint, and the operation under consideration. A more readable result is insufficient evidence that a transformation preserved meaning.

For malformed input, establish the selected decoder’s documented error behavior before prescribing recovery. Check what it reports, whether it provides a result, and what the application is expected to do with that outcome. Define rejection, correction, or another explicit handling policy only after those expectations are known.

An actionable error report connects the observation to the failed contract. Include the affected field, the available original text, and the expected representation. This gives the next investigation a concrete starting point.

Verify recovered data and URL structure

Verify the repair through the actual serialization and parsing boundaries. Assert the recovered value and the surrounding structure separately so a correct-looking value does not hide changed parameter boundaries.

Fixture Required check
Space The receiver obtains the intended space.
% and literal %20 Literal percent text survives as specified.
+ The intended plus character is recovered under the chosen convention.
& and = The data stays inside the expected parameter.
/ and literal %2F Each distinct input produces its specified receiver value.
Unicode text The recovered text matches the fixture.
Repeated parameters Every expected entry and value remains present.
An inner URL Each documented parsing step yields its specified representation.

For repeated parameters, list all expected entries explicitly. For nested URLs, assert intermediate results as well as the final value. Retain the original failing example as a regression fixture with named checkpoints.

Also compare the representation used for validation with the one consumed downstream. A decoding discrepancy between filtering and later consumption can allow harmful input to bypass the filter. Verify that those stages apply compatible decoding steps.

Accept the repair when the trace explains each required layer, the receiver obtains the intended text, and structural checks pass.

Previous note