Broken redirects, double-encoded query params, and "works in the browser but fails in fetch" bugs usually trace back to URL structure — not application logic. A URL is not a opaque string; it is a grammar with reserved characters and encoding rules. This guide dissects each part, shows how query strings should be built, and clarifies encodeURIComponent versus encodeURI.
Bottom line: Parse before you mutate. Encode values with encodeURIComponent (or URLSearchParams); never encode an entire URL with encodeURIComponent. Inspect structure in a URL Parser, build or parse params with the Query String Builder, and test encodings with URL Encode/Decode.
The parts of a URL
https://user:pass@www.example.com:443/path/to/page?query=1&sort=asc#section
└─┬─┘ └─┬─┘ └──────┬──────┘ └┬┘ └────┬────┘ └──────┬──────┘ └───┬───┘
scheme userinfo host port path query fragment
| Part |
Example |
Notes |
| Scheme |
https |
Protocol; affects default port |
| Userinfo |
user:pass |
Rare in modern apps; avoid embedding passwords |
| Host |
www.example.com |
Domain or IP |
| Port |
443 |
Omitted when default for scheme |
| Path |
/path/to/page |
Hierarchical segments |
| Query |
query=1&sort=asc |
Key/value; order may or may not matter to your app |
| Fragment |
section |
Client-only; not sent to the server |
Paste any production URL into a URL Parser to see these fields split cleanly — especially when debugging redirects that drop the query or hash.
Absolute vs relative
https://example.com/tools/json-formatter → absolute
/tools/json-formatter → path-absolute
?page=2 → query-relative to current URL
#faq → fragment-only
fetch('/api') resolves against the current origin. new URL('/api', 'https://example.com') makes resolution explicit — prefer that in non-browser tooling.
Canonical form:
?search=json+formatter&page=2&tags=css&tags=html
Rules that prevent bugs:
- Keys and values should be percent-encoded.
- Use
& between pairs; = between key and value.
- Spaces become
%20 (or + in application/x-www-form-urlencoded bodies — know which context you are in).
- Repeated keys may mean arrays — agree with your backend.
Prefer URL and URLSearchParams
const url = new URL("https://example.com/search");
url.searchParams.set("q", "css clamp()");
url.searchParams.set("page", "1");
console.log(url.toString());
// https://example.com/search?q=css+clamp%28%29&page=1
URLSearchParams encodes values correctly. Manual string concat is how % and & inside values corrupt the query.
// Fragile
const bad = `https://api.example.com/items?name=${name}¬e=${note}`;
// Safe
const url = new URL("https://api.example.com/items");
url.searchParams.set("name", name);
url.searchParams.set("note", note);
encodeURIComponent vs encodeURI
Both are JavaScript helpers for percent-encoding. They are not interchangeable.
| Function |
Encodes spaces etc. |
Leaves intact |
Use for |
encodeURIComponent |
Yes |
A-Z a-z 0-9 - _ . ! ~ * ' ( ) |
Single query value, path segment, or form field |
encodeURI |
Yes |
Reserved URL punctuation: ; , / ? : @ & = + $ # among others |
Rarely — almost whole URLs that are already structured |
encodeURIComponent("a&b=c");
// "a%26b%3Dc"
encodeURI("https://example.com/a&b=c");
// "https://example.com/a&b=c" — & not encoded; still a broken query if you meant one value
Common mistake: encoding the whole URL
// Wrong — breaks the URL
fetch(encodeURIComponent("https://example.com/api?q=1"));
// Right — encode only the value
fetch("https://example.com/api?q=" + encodeURIComponent("1 & 2"));
// Better
const u = new URL("https://example.com/api");
u.searchParams.set("q", "1 & 2");
fetch(u);
Round-trip experiments belong in URL Encode/Decode: encode a value, decode it, confirm you did not double-encode (%2526 instead of %26).
Reserved and unsafe characters
RFC 3986 reserved characters have special meaning in URLs:
:/?#[]@!$&'()*+,;=
If a value must contain them, percent-encode. If you leave & raw inside a value, parsers treat it as a separator.
| Character |
Often encoded as |
Why |
| Space |
%20 |
Separators / readability |
& |
%26 |
Query pair delimiter |
= |
%3D |
Key/value delimiter |
# |
%23 |
Starts fragment |
? |
%3F |
Starts query |
/ |
%2F |
Path segment boundary (when inside a segment) |
% |
%25 |
Escape character itself |
+ |
%2B |
Ambiguous with form space-as-plus |
Path segments vs query
// Path segment with a slash in the id — encode the segment
const id = "a/b";
const path = `/items/${encodeURIComponent(id)}`;
// /items/a%2Fb
Slugs for blogs and tools are a different concern: human-readable path segments usually restrict to safe characters. A Slug Generator turns titles into lowercase-hyphenated paths so you avoid encoding surprises in public URLs.
Double encoding and decoding bugs
Symptoms:
- Logs show
%253A instead of %3A (encoded % then :).
- Server receives literal
%20 instead of a space.
- Redirect loops that re-encode an already-encoded
Location.
Discipline:
- Keep data decoded in application memory.
- Encode once at the HTTP boundary (building a URL or header).
- Decode once when parsing a request.
- Never run
encodeURIComponent on a string that is already a full encoded URL.
Use a URL Parser to see whether a mysterious string is a full URL or a lone encoded value.
Fragments, tracking params, and privacy
- Fragments (
#...) stay in the browser — useful for in-page navigation; useless for server analytics.
- UTM and referral params pollute canonical URLs — strip them for
rel=canonical and sharing when they are not part of content identity.
- Tokens in query strings leak via
Referer, logs, and browser history — prefer headers or fragments carefully (fragments still appear in history).
Internationalized domains and paths
User-facing URLs may include non-ASCII characters. Under the hood:
| Layer |
Encoding |
| Domain (IDN) |
Punycode in DNS (bücher.de → xn--bcher-kva.de) |
| Path / query |
UTF-8 percent-encoding (%C3%A9 for é) |
const url = new URL("https://example.com/");
url.pathname = "/café";
url.searchParams.set("q", "naïve");
console.log(url.href);
// https://example.com/caf%C3%A9?q=na%C3%AFve
When comparing URLs in tests, normalize with the URL API rather than raw string equality — browsers may serialize the same resource differently (%7E vs ~).
Debugging checklist
Practical takeaway
Treat URLs as structured data: scheme, host, path, query, fragment. Build queries with URL / URLSearchParams, encode individual values with encodeURIComponent, and leave encodeURI alone unless you know why you need it. Debug with a URL Parser and URL Encode/Decode; create safe public paths with a Slug Generator. Encode once at the edge — never trust hand-concatenated strings with user input.
Sources