Percent-Encoding — Path vs Query vs Fragment
A support ticket lands with a “broken share link.” Someone pasted a full URL into a helper that ran encodeURIComponent on everything. The result starts with https%3A%2F%2F… and nowhere is that a valid navigation target. Percent-encoding is not one global switch — it is a set of rules that change by which part of the URL you are writing.
Three contexts, three rules
| Part | Typical characters that must be encoded | Safe to leave alone |
|---|---|---|
| Path segment | space, ?, #, / (if it is data, not a separator) | unreserved letters/digits -._~ |
| Query value | space, &, =, #, non-ASCII | structure of ?key=value&… itself |
| Fragment | space and reserved chars the fragment parser cares about | # that starts the fragment |
Encoding the structure of a URL (scheme, ://, path slashes that mean hierarchy, ? that starts the query) is how you destroy it. Encoding the payload inside a segment or a value is how you keep it intact.
encodeURI vs encodeURIComponent
In browsers and Node:
encodeURI— meant for a full URL string. It leaves: / ? #and similar structural characters alone. Useful when you already have a mostly-valid URL and only need to fix unsafe bytes.encodeURIComponent— meant for a single component. It encodes almost everything reserved, including/and?. Correct for a search term, a filename in a path segment, or a query parameter value.
Rule of thumb: build URLs from pieces. Encode each piece with encodeURIComponent, then join with literal ?, &, =, and /.
const base = "https://api.example.com/v1/search";
const q = encodeURIComponent("c++ & rust");
const url = `${base}?q=${q}`;
// https://api.example.com/v1/search?q=c%2B%2B%20%26%20rust
Path traps people hit in production
Filenames with spaces — /files/My Report.pdf should become /files/My%20Report.pdf. Leaving the space works in some browsers and fails in others or in CDNs.
Slashes inside an ID — If the ID is a/b, that is two path segments unless you encode: /items/a%2Fb. Frameworks that decode once and re-split on / still surprise teams; know whether your router decodes before matching.
Unicode — Usernames and product slugs with non-ASCII should be UTF-8 then percent-encoded. Copy-paste from a spreadsheet often introduces “smart” punctuation that looks fine in the UI and 404s on the server.
Query string specifics
& and = separate pairs. If a value contains them, encode the value, never the whole query string as one blob after the fact.
# Wrong mental model
encodeURIComponent("?q=a&b=1") // encodes the ? and & — now one useless parameter
# Right
"?q=" + encodeURIComponent("a&b=1")
Some legacy stacks still emit + for spaces in queries. When debugging redirects, decode and compare the logical string, not the wire form.
Fragments and client-only state
Anything after # is not sent to the server on a normal navigation. Encoding still matters for in-page routers and OAuth-style flows that stash state in the hash. Do not assume the server “saw” a broken fragment — it never arrived.
Double-encoding: how to unwind it
Symptoms: logs show %252F or %253A. Something encoded an already-encoded string.
- Decode once; if the result still looks encoded, decode again until it stabilizes or looks like plain text.
- Decide the target context (path vs query).
- Encode once for that context.
- Persist the canonical form so the next hop does not encode again.
Use url-encode when you need a quick encode/decode pass on a suspicious string without shipping it to a remote service — paste the path segment or query value alone, not the whole URL, unless you intentionally chose encodeURI-style behavior.
Practical checklist before you ship a link builder
- Never run
encodeURIComponenton a full absolute URL. - Encode path segments and query values independently.
- Prefer
%20for spaces unless you are speaking form-urlencoded to a known peer. - Log both raw and decoded forms when investigating 404s on “weird” URLs.
- Treat
%in user input as data — encode it to%25so a later decode does not invent new sequences.
Percent-encoding is boring until one wrong helper turns every share link into percent soup. Keep structure literal, encode payloads, and decode before you re-encode when cleaning legacy data.