<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Url-Encoding | Toolbox Guides &amp; Articles</title><link>https://blog.toolbox.vishnudigital.com/tags/url-encoding/</link><atom:link href="https://blog.toolbox.vishnudigital.com/tags/url-encoding/index.xml" rel="self" type="application/rss+xml"/><description>Url-Encoding</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sun, 06 Sep 2026 00:00:00 +0000</lastBuildDate><image><url>https://blog.toolbox.vishnudigital.com/media/icon.svg</url><title>Url-Encoding</title><link>https://blog.toolbox.vishnudigital.com/tags/url-encoding/</link></image><item><title>URL Percent-Encoding Explained: RFC 3986, encodeURIComponent, and the %20 vs + Trap</title><link>https://blog.toolbox.vishnudigital.com/url-percent-encoding-rfc-3986-guide/</link><pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate><guid>https://blog.toolbox.vishnudigital.com/url-percent-encoding-rfc-3986-guide/</guid><description>
&lt;aside class="comic-bubble not-prose my-8 p-6 rounded-2xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#fdfbf7] dark:bg-[#28201a] shadow-[4px_4px_0px_#2c221a] dark:shadow-[4px_4px_0px_#14100d]" aria-label="AEO Quick Answer &amp; Specification Dossier"&gt;
&lt;div class="flex items-center justify-between gap-3 mb-3"&gt;
&lt;div class="flex items-center gap-2"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2.5 py-0.5 rounded text-[11px] font-black tracking-wider"&gt;QUICK ANSWER&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold text-[#2c221a]/70 dark:text-[#ede6db]/70"&gt;RFC SPECIFICATION SUMMARY&lt;/span&gt;
&lt;/div&gt;
&lt;span class="hidden sm:inline-flex items-center gap-1.5 text-[11px] font-bold font-mono px-2 py-0.5 rounded border border-emerald-700 dark:border-emerald-500 bg-emerald-100 dark:bg-emerald-950 text-emerald-900 dark:text-emerald-200"&gt;
&lt;span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-600 dark:bg-emerald-400"&gt;&lt;/span&gt; 100% Client-Side
&lt;/span&gt;
&lt;/div&gt;
&lt;p class="text-base sm:text-lg text-[#2c221a] dark:text-[#ede6db] leading-relaxed font-semibold mb-4"&gt;
Percent-encoding replaces a character with a percent sign followed by two hexadecimal digits representing its UTF-8 bytes. RFC 3986 leaves only the unreserved set unencoded: A-Z, a-z, 0-9, hyphen, period, underscore, and tilde. In JavaScript, use encodeURIComponent for individual query parameter values and path segments because it escapes reserved delimiters, and encodeURI only for an entire URL because it deliberately preserves the structural characters that separate the scheme, host, path, query, and fragment.
&lt;/p&gt;
&lt;div class="flex flex-wrap gap-2 text-xs font-mono font-bold mt-4 pt-3 border-t border-[#ded4c3] dark:border-[#3b332c]"&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Difficulty: Beginner&lt;/span&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Time: 6 Mins&lt;/span&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Privacy: 100% Client-Side RAM&lt;/span&gt;
&lt;span class="px-2.5 py-1 rounded bg-[#ede6db] dark:bg-[#382e26] text-[#2c221a] dark:text-[#ede6db] border border-[#ded4c3] dark:border-[#4a3e35]"&gt;Standard: RFC 3986&lt;/span&gt;
&lt;/div&gt;
&lt;/aside&gt;
&lt;div class="tool-embed-container comic-panel not-prose my-10 rounded-2xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#fdfbf7] dark:bg-[#28201a] shadow-[4px_4px_0px_#2c221a] dark:shadow-[4px_4px_0px_#14100d] overflow-hidden" id="embed-url-encode-1788851897585507000-box" role="region" aria-label="Interactive Tool Workbench"&gt;
&lt;div class="comic-halftone p-4 sm:p-5 border-b-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#f5efe4] dark:bg-[#211a14] flex flex-wrap items-center justify-between gap-3"&gt;
&lt;div class="flex items-center gap-2.5 flex-wrap min-w-0"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2.5 py-0.5 rounded text-[10px] font-black tracking-wider uppercase"&gt;
Interactive Workbench
&lt;/span&gt;
&lt;span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-mono font-bold bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-300 dark:border-emerald-800 shrink-0"&gt;
&lt;span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"&gt;&lt;/span&gt;
&lt;span&gt;LIVE&lt;/span&gt;
&lt;/span&gt;
&lt;h3 class="text-base sm:text-lg font-extrabold text-[#2c221a] dark:text-[#ede6db] tracking-tight truncate m-0"&gt;
URL Percent-Encoding Explained: RFC 3986, encodeURIComponent, and the %20 vs &amp;#43; Trap
&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="flex items-center gap-2 shrink-0"&gt;
&lt;a href="https://toolbox.vishnudigital.com/url-encode" target="_blank" rel="noopener noreferrer" class="comic-btn-secondary !py-1 !px-3 !text-xs font-mono font-bold flex items-center gap-1.5 shadow-[2px_2px_0px_#2c221a] dark:shadow-[2px_2px_0px_#14100d] hover:translate-x-[-1px] hover:translate-y-[-1px] transition-all" title="Open full utility in new window"&gt;
&lt;span&gt;Open Fullscreen ↗&lt;/span&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="relative w-full overflow-hidden bg-[#fdfbf7] dark:bg-[#1a1410]" style="min-height: 420px;"&gt;
&lt;div id="embed-url-encode-1788851897585507000-skeleton" class="absolute inset-0 flex flex-col items-center justify-center p-8 bg-[#fdfbf7] dark:bg-[#1e1814] z-10 transition-opacity duration-300"&gt;
&lt;div class="flex items-center gap-3 p-4 rounded-xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#f5efe4] dark:bg-[#28201a] shadow-[2px_2px_0px_#2c221a] dark:shadow-[2px_2px_0px_#14100d] max-w-sm w-full"&gt;
&lt;div class="w-8 h-8 rounded-lg bg-amber-500/20 flex items-center justify-center shrink-0"&gt;
&lt;svg class="w-5 h-5 text-amber-600 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"&gt;
&lt;circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"&gt;&lt;/circle&gt;
&lt;path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;div class="min-w-0 flex-1"&gt;
&lt;div class="text-xs font-bold text-[#2c221a] dark:text-[#ede6db] truncate"&gt;Initializing Workbench...&lt;/div&gt;
&lt;div class="text-[10px] font-mono text-[#2c221a]/60 dark:text-[#ede6db]/60"&gt;100% Client-Side RAM Sandbox&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;iframe
id="embed-url-encode-1788851897585507000-frame"
data-base-src="https://toolbox.vishnudigital.com/url-encode?embed=true"
src="about:blank"
title="URL Percent-Encoding Explained: RFC 3986, encodeURIComponent, and the %20 vs &amp;#43; Trap – Free Online Utility"
class="w-full border-0 block opacity-0 transition-opacity duration-300"
style="height: 560px; min-height: 420px;"
loading="lazy"
allow="clipboard-read; clipboard-write; web-share"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads"&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;div class="p-3 sm:px-5 border-t border-[#ded4c3] dark:border-[#3b332c] bg-[#f5efe4]/80 dark:bg-[#211a14]/80 flex flex-wrap items-center justify-between gap-3 text-xs font-mono"&gt;
&lt;div class="flex items-center gap-2 text-[#2c221a]/70 dark:text-[#ede6db]/70"&gt;
&lt;span class="text-emerald-600 dark:text-emerald-400 font-bold"&gt;🔒 Private Execution:&lt;/span&gt;
&lt;span class="hidden sm:inline"&gt;Computations run in local browser memory. Zero server uploads.&lt;/span&gt;
&lt;span class="sm:hidden"&gt;Zero server uploads.&lt;/span&gt;
&lt;/div&gt;
&lt;div class="flex items-center gap-1.5 shrink-0"&gt;
&lt;a href="https://toolbox.vishnudigital.com/url-encode" target="_blank" rel="noopener noreferrer" class="font-bold text-amber-700 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-300 hover:underline"&gt;
Launch Fullscreen →
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(function() {
const containerId = "embed-url-encode-1788851897585507000";
const frame = document.getElementById(containerId + "-frame");
const skeleton = document.getElementById(containerId + "-skeleton");
if (!frame) return;
function getCurrentTheme() {
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}
function resolveSrc() {
const baseSrc = frame.getAttribute("data-base-src");
if (!baseSrc) return "";
const theme = getCurrentTheme();
return baseSrc + (baseSrc.includes("?") ? "&amp;" : "?") + "theme=" + theme;
}
function loadIframe() {
if (frame.getAttribute("src") !== "about:blank") return;
const fullSrc = resolveSrc();
frame.src = fullSrc;
frame.onload = function() {
if (skeleton) {
skeleton.classList.add("opacity-0");
setTimeout(function() { skeleton.style.display = "none"; }, 300);
}
frame.classList.remove("opacity-0");
frame.classList.add("opacity-100");
syncThemeToIframe();
};
}
function syncThemeToIframe() {
if (!frame || !frame.contentWindow) return;
try {
frame.contentWindow.postMessage({
type: "TOOLBOX_THEME_CHANGE",
theme: getCurrentTheme()
}, "*");
} catch(e) {}
}
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
loadIframe();
observer.disconnect();
}
});
}, { rootMargin: "250px" });
observer.observe(frame);
} else {
loadIframe();
}
window.addEventListener("message", function(event) {
if (
event.data &amp;&amp;
typeof event.data === "object" &amp;&amp;
event.data.type === "TOOLBOX_RESIZE" &amp;&amp;
event.data.height
) {
if (event.source === frame.contentWindow) {
const measured = Math.max(event.data.height + 20, 420);
frame.style.height = measured + "px";
}
}
});
const themeObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.attributeName === "class") {
syncThemeToIframe();
}
});
});
themeObserver.observe(document.documentElement, { attributes: true });
document.querySelectorAll(".theme-toggle").forEach(function(btn) {
btn.addEventListener("click", function() {
setTimeout(syncThemeToIframe, 50);
});
});
})();
&lt;/script&gt;
&lt;div class="tool-hero-cta not-prose my-10 p-6 sm:p-7 rounded-2xl border-2 border-[#2c221a] dark:border-[#ded4c3] bg-[#fdfbf7] dark:bg-[#28201a] shadow-[4px_4px_0px_#2c221a] dark:shadow-[4px_4px_0px_#14100d] flex flex-col md:flex-row items-start md:items-center justify-between gap-6 transition-all" role="complementary" aria-label="Interactive Companion Tool"&gt;
&lt;div class="space-y-2 max-w-xl"&gt;
&lt;div class="flex items-center gap-2"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2.5 py-0.5 rounded text-[10px] font-black tracking-wider uppercase"&gt;
Free In-Browser Tool
&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold text-stone-500 dark:text-stone-400"&gt;TOOLBOX UTILITY&lt;/span&gt;
&lt;/div&gt;
&lt;h3 class="text-xl font-extrabold text-[#2c221a] dark:text-[#ede6db] tracking-tight m-0"&gt;
URL Percent Encoder &amp;amp; Decoder
&lt;/h3&gt;
&lt;p class="text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium m-0"&gt;
Switch between Encode and Decode tabs, paste any string, and get instant UTF-8-correct percent-encoding with one-click copy.
&lt;/p&gt;
&lt;/div&gt;
&lt;div class="shrink-0 w-full md:w-auto"&gt;
&lt;a href="https://toolbox.vishnudigital.com/url-encode" class="comic-btn-primary w-full md:w-auto text-center" &gt;
Open URL Encoder &amp;amp; Decoder →
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-urls-need-encoding-at-all"&gt;Why URLs Need Encoding at All&lt;/h2&gt;
&lt;p&gt;A URL is not free-form text. It is a structured identifier in which certain characters carry meaning: &lt;code&gt;?&lt;/code&gt; starts the query, &lt;code&gt;&amp;amp;&lt;/code&gt; separates parameters, &lt;code&gt;#&lt;/code&gt; begins the fragment, &lt;code&gt;/&lt;/code&gt; divides path segments.&lt;/p&gt;
&lt;p&gt;The moment user data containing those characters is dropped into a URL, the structure collapses:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;https://example.com/search?q=fish &amp;amp; chips#1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ^ ^ ^
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | └── parsed as a fragment
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | └──────── parsed as a parameter separator
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─────────────── raw space, invalid in a URL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The server sees the parameter &lt;code&gt;q&lt;/code&gt; with value &lt;code&gt;fish &lt;/code&gt;, a second empty parameter named &lt;code&gt;chips&lt;/code&gt;, and a fragment of &lt;code&gt;1&lt;/code&gt;. Percent-encoding solves this by giving every problematic character an unambiguous escaped form.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;https://example.com/search?q=fish%20%26%20chips%231
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="the-rfc-3986-character-classes"&gt;The RFC 3986 Character Classes&lt;/h2&gt;
&lt;p&gt;Everything follows from three sets defined in &lt;strong&gt;RFC 3986&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="unreserved--safe-everywhere-never-needs-encoding"&gt;Unreserved — safe everywhere, never needs encoding&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;A-Z a-z 0-9 - . _ ~
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Section 2.3 states these are equivalent whether escaped or not. Encoding them is legal but pointless, and normalisers will decode them back.&lt;/p&gt;
&lt;h3 id="reserved--structurally-meaningful-must-be-encoded-inside-data"&gt;Reserved — structurally meaningful, must be encoded inside data&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Characters&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gen-delims&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt; &lt;code&gt;/&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; &lt;code&gt;#&lt;/code&gt; &lt;code&gt;[&lt;/code&gt; &lt;code&gt;]&lt;/code&gt; &lt;code&gt;@&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Separate the major URL components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;sub-delims&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;!&lt;/code&gt; &lt;code&gt;$&lt;/code&gt; &lt;code&gt;&amp;amp;&lt;/code&gt; &lt;code&gt;'&lt;/code&gt; &lt;code&gt;(&lt;/code&gt; &lt;code&gt;)&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;+&lt;/code&gt; &lt;code&gt;,&lt;/code&gt; &lt;code&gt;;&lt;/code&gt; &lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delimiters inside a component&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="everything-else--must-be-encoded"&gt;Everything else — must be encoded&lt;/h3&gt;
&lt;p&gt;Spaces, control characters, &lt;code&gt;&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;\&lt;/code&gt;, &lt;code&gt;^&lt;/code&gt;, &lt;code&gt;`&lt;/code&gt;, &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, and every non-ASCII character.&lt;/p&gt;
&lt;h3 id="the-percent-encoding-rule"&gt;The Percent-Encoding Rule&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;character -&amp;gt; UTF-8 bytes -&amp;gt; %HH for each byte
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Character&lt;/th&gt;
&lt;th&gt;UTF-8 Bytes&lt;/th&gt;
&lt;th&gt;Encoded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;space&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%20&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;26&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%26&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%2F&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%25&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;é&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C3 A9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%C3%A9&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;€&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E2 82 AC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%E2%82%AC&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;🚀&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F0 9F 9A 80&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%F0%9F%9A%80&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RFC 3986 §2.1 says the hexadecimal digits &lt;strong&gt;should be uppercase&lt;/strong&gt;; normalisers uppercase them, and &lt;code&gt;%2f&lt;/code&gt; and &lt;code&gt;%2F&lt;/code&gt; are equivalent on decode.&lt;/p&gt;
&lt;div class="in-article-ad-wrapper not-prose my-8 p-3 sm:p-4 rounded-xl border-2 border-stone-900/40 dark:border-stone-400/40 bg-[#fdfbf7] dark:bg-[#211c18] shadow-[3px_3px_0px_rgba(41,30,20,0.3)] dark:shadow-[3px_3px_0px_rgba(242,237,228,0.2)] overflow-hidden"&gt;
&lt;div class="flex items-center justify-between mb-2 border-b border-stone-200 dark:border-stone-800 pb-1"&gt;
&lt;span class="text-[10px] font-mono font-bold uppercase tracking-widest text-stone-500 dark:text-stone-400"&gt;Advertisement&lt;/span&gt;
&lt;span class="text-[9px] font-mono uppercase px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300"&gt;Sponsored&lt;/span&gt;
&lt;/div&gt;
&lt;div class="w-full flex justify-center items-center overflow-hidden min-h-[250px]"&gt;
&lt;ins class="adsbygoogle"
style="display:block; text-align:center; min-height:250px; width:100%;"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-5474548764059850"
&gt;&lt;/ins&gt;
&lt;script&gt;
(adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id="the-three-javascript-functions"&gt;The Three JavaScript Functions&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Leaves Unencoded&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encodeURIComponent()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;A-Z a-z 0-9 - _ . ! ~ * ' ( )&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Use this for values&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encodeURI()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The above &lt;strong&gt;plus&lt;/strong&gt; &lt;code&gt;; / ? : @ &amp;amp; = + $ , #&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only for a whole URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;escape()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy, emits non-standard &lt;code&gt;%uXXXX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Never use&lt;/strong&gt; — deprecated in Annex B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="the-decision-rule"&gt;The Decision Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Encoding a piece of data?&lt;/strong&gt; &lt;code&gt;encodeURIComponent&lt;/code&gt;.
&lt;strong&gt;Encoding an already-assembled URL?&lt;/strong&gt; &lt;code&gt;encodeURI&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;fish &amp;amp; chips&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://example.com/next?id=7&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Correct — each value encoded independently.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sb"&gt;`https://example.com/search?q=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sb"&gt;`&amp;amp;next=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Wrong — encodeURI leaves &amp;amp; and = intact, so the nested URL&amp;#39;s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// own parameters merge into the outer query string.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;broken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;encodeURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`https://example.com/search?next=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That second case — a URL nested inside a query parameter — is the classic failure. &lt;code&gt;encodeURI&lt;/code&gt; preserves &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;&amp;amp;&lt;/code&gt; because it assumes they are &lt;em&gt;your&lt;/em&gt; delimiters, and an OAuth &lt;code&gt;redirect_uri&lt;/code&gt; or a &lt;code&gt;?next=&lt;/code&gt; parameter promptly falls apart.&lt;/p&gt;
&lt;h3 id="where-encodeuricomponent-diverges-from-rfc-3986"&gt;Where &lt;code&gt;encodeURIComponent&lt;/code&gt; Diverges from RFC 3986&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;encodeURIComponent&lt;/code&gt; leaves &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;'&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;, and &lt;code&gt;*&lt;/code&gt; unescaped, although RFC 3986 classifies them as sub-delims rather than unreserved. Most servers cope, but strict parsers and signature-verifying APIs — AWS Signature V4 and OAuth 1.0a among them — do not. For strict compliance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;rfc3986&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sr"&gt;/[!&amp;#39;()*]/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="20-or--two-specifications-one-space"&gt;&lt;code&gt;%20&lt;/code&gt; or &lt;code&gt;+&lt;/code&gt;: Two Specifications, One Space&lt;/h2&gt;
&lt;p&gt;This trips up experienced engineers because &lt;strong&gt;both are correct — in different contexts&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Space Becomes&lt;/th&gt;
&lt;th&gt;Governing Specification&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Path segment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RFC 3986&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fragment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RFC 3986&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encodeURIComponent()&lt;/code&gt; output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ECMA-262&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML form &lt;code&gt;GET&lt;/code&gt; submission&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;URLSearchParams&lt;/code&gt; serialisation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WHATWG URL Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;fish &amp;amp; chips&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// &amp;#34;q=fish+%26+chips&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fish &amp;amp; chips&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;#34;fish%20%26%20chips&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both round-trip correctly through a &lt;strong&gt;form-encoding&lt;/strong&gt; parser. But a generic RFC 3986 parser reading a &lt;em&gt;path&lt;/em&gt; decodes &lt;code&gt;+&lt;/code&gt; as a literal plus sign. The practical consequences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;+&lt;/code&gt; in a path segment is a plus, not a space.&lt;/strong&gt; &lt;code&gt;/files/my+file.txt&lt;/code&gt; refers to a file whose name contains a plus.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encode a literal plus in form data as &lt;code&gt;%2B&lt;/code&gt;.&lt;/strong&gt; Otherwise &lt;code&gt;user+tag@example.com&lt;/code&gt; arrives as &lt;code&gt;user tag@example.com&lt;/code&gt; — the reason so many sites mangle plus-addressed email.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick one serialiser per codebase.&lt;/strong&gt; Mixing &lt;code&gt;URLSearchParams&lt;/code&gt; with hand-built strings produces query strings that decode inconsistently.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="failure-modes-worth-recognising"&gt;Failure Modes Worth Recognising&lt;/h2&gt;
&lt;h3 id="1-double-encoding"&gt;1. Double Encoding&lt;/h3&gt;
&lt;p&gt;Percent-encoding is &lt;strong&gt;not idempotent&lt;/strong&gt;, because &lt;code&gt;%&lt;/code&gt; is itself an encodable character.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;a b&amp;#34; -&amp;gt; encode -&amp;gt; &amp;#34;a%20b&amp;#34; -&amp;gt; encode again -&amp;gt; &amp;#34;a%2520b&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The server decodes once and hands your application the literal string &lt;code&gt;a%20b&lt;/code&gt;. Symptom: values that visibly contain &lt;code&gt;%20&lt;/code&gt;, &lt;code&gt;%3D&lt;/code&gt;, or &lt;code&gt;%26&lt;/code&gt; after parsing. Cause: a value encoded at construction time and encoded again by a helper, a proxy, or a template. Encode exactly once, as late as possible.&lt;/p&gt;
&lt;h3 id="2-encoded-slashes-rejected-by-the-server"&gt;2. Encoded Slashes Rejected by the Server&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;%2F&lt;/code&gt; inside a path segment is legal, but Apache HTTP Server defaults to &lt;code&gt;AllowEncodedSlashes Off&lt;/code&gt; and returns &lt;strong&gt;404&lt;/strong&gt; for such requests. Never place a value that might contain &lt;code&gt;/&lt;/code&gt; into a path segment — put it in the query string instead.&lt;/p&gt;
&lt;h3 id="3-urierror-uri-malformed"&gt;3. &lt;code&gt;URIError: URI malformed&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;decodeURIComponent&lt;/code&gt; throws on a truncated or invalid escape such as &lt;code&gt;%E0%A4&lt;/code&gt; or &lt;code&gt;%ZZ&lt;/code&gt;. Any decode of untrusted input needs a guard:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;safeDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Malformed input — return it untouched rather than crashing.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-non-ascii-hostnames-are-not-percent-encoded"&gt;4. Non-ASCII Hostnames Are Not Percent-Encoded&lt;/h3&gt;
&lt;p&gt;Percent-encoding applies to the path, query, and fragment. &lt;strong&gt;Internationalised domain names use Punycode&lt;/strong&gt; (RFC 3492/5891) instead: &lt;code&gt;münchen.de&lt;/code&gt; becomes &lt;code&gt;xn--mnchen-3ya.de&lt;/code&gt;. Encoding a hostname with &lt;code&gt;encodeURIComponent&lt;/code&gt; produces an unresolvable name.&lt;/p&gt;
&lt;h3 id="5-encoded-data-in-logs-is-still-sensitive"&gt;5. Encoded Data in Logs Is Still Sensitive&lt;/h3&gt;
&lt;p&gt;Percent-encoding is not obfuscation. Anything placed in a query string lands in server access logs, proxy caches, &lt;code&gt;Referer&lt;/code&gt; headers, and browser history. Tokens and secrets belong in headers or a request body — a point worth remembering when
.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="when-to-reach-for-base64-instead"&gt;When to Reach for Base64 Instead&lt;/h2&gt;
&lt;p&gt;Percent-encoding is designed for text that is &lt;em&gt;mostly&lt;/em&gt; URL-safe already. Encoding binary data with it is wasteful — every byte becomes three characters.&lt;/p&gt;
&lt;p&gt;For binary payloads inside a URL, use &lt;strong&gt;base64url&lt;/strong&gt; (RFC 4648 §5), which substitutes &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; for &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; and drops padding, producing a string made entirely of unreserved characters. This is exactly what JWT segments use. The
covers the mechanics, and the
shows base64url in production.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="step-by-step-encoding-with-toolbox"&gt;Step-by-Step: Encoding with Toolbox&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open the tool&lt;/strong&gt;: visit the
.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose a tab&lt;/strong&gt; — &lt;strong&gt;Encode&lt;/strong&gt; to convert raw text into percent-encoded form, &lt;strong&gt;Decode&lt;/strong&gt; to reverse it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paste the value, not the whole URL.&lt;/strong&gt; Encode one query parameter value or one path segment at a time, then assemble the URL yourself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read the output&lt;/strong&gt; — the encoder applies the browser&amp;rsquo;s native &lt;code&gt;encodeURIComponent&lt;/code&gt;, so UTF-8 multi-byte characters convert correctly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copy the result&lt;/strong&gt; and drop it into your request, config file, or test case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diagnose double encoding&lt;/strong&gt; by pasting a suspect value into the Decode tab. If one pass still leaves &lt;code&gt;%&lt;/code&gt; sequences behind, the value was encoded twice upstream.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Encoding runs entirely on your device through the browser&amp;rsquo;s own URL functions, so tokens, search terms, and customer identifiers in the strings you are debugging are never transmitted.&lt;/p&gt;
&lt;section class="faqs-container not-prose my-12 border-t-2 border-stone-200 dark:border-stone-800 pt-8" aria-label="Frequently Asked Questions"&gt;
&lt;div class="flex items-center gap-2 mb-6"&gt;
&lt;span class="comic-stamp bg-amber-600 text-white px-2 py-0.5 rounded text-[10px] font-black tracking-wider"&gt;FAQ&lt;/span&gt;
&lt;h2 class="text-2xl font-extrabold text-[#2c221a] dark:text-[#ede6db] tracking-tight m-0"&gt;
Frequently Asked Questions
&lt;/h2&gt;
&lt;/div&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;What is the difference between encodeURI and encodeURIComponent?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
encodeURI is for a complete URL and deliberately leaves the reserved structural characters ; / ? : @ &amp;amp; = &amp;#43; $ , # untouched so the URL keeps working. encodeURIComponent is for a single piece of data being placed inside a URL, such as one query parameter value, and escapes those reserved characters as well. Use encodeURIComponent for values and encodeURI only when handling an entire URL string.
&lt;/p&gt;
&lt;/details&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;Why does a space sometimes become %20 and sometimes a plus sign?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
Percent-encoding under RFC 3986 always represents a space as %20. The plus sign comes from a different and older specification, application/x-www-form-urlencoded, which HTML forms and the URLSearchParams API use for query strings. Both forms are decoded as a space by a form-encoding parser, but a generic RFC 3986 parser treats a literal plus as a plus, which is why the same string can decode differently on two servers.
&lt;/p&gt;
&lt;/details&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;Which characters never need to be percent-encoded?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
RFC 3986 section 2.3 defines the unreserved set as the ASCII letters A to Z and a to z, the digits 0 to 9, and the four symbols hyphen, period, underscore, and tilde. These characters are equivalent whether encoded or not, so encoding them adds length without changing meaning. Every other character is either reserved, meaning it has structural significance, or must be encoded.
&lt;/p&gt;
&lt;/details&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;Why did my URL break after being encoded twice?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
Percent-encoding is not idempotent. The percent sign is itself a character that must be encoded as %25, so running an already-encoded string through the encoder again turns %20 into %2520. The receiving server decodes only once and reads a literal %20 in the value. Encode exactly once, at the point where a raw value is inserted into a URL, and never on a string that already contains percent-escapes.
&lt;/p&gt;
&lt;/details&gt;
&lt;details name="faq" class="group border-2 border-[#2c221a] dark:border-[#ded4c3] rounded-xl p-4 bg-[#fdfbf7] dark:bg-[#28201a] shadow-[3px_3px_0px_#2c221a] dark:shadow-[3px_3px_0px_#14100d] mb-3 transition-all"&gt;
&lt;summary class="flex items-center justify-between cursor-pointer font-bold text-[#2c221a] dark:text-[#ede6db] select-none text-base"&gt;
&lt;span&gt;How are non-ASCII characters such as accented letters encoded?&lt;/span&gt;
&lt;span class="text-xs font-mono font-bold transition-transform duration-200 group-open:rotate-180 shrink-0 ml-2"&gt;▼&lt;/span&gt;
&lt;/summary&gt;
&lt;p class="mt-3 text-sm text-stone-600 dark:text-stone-300 leading-relaxed font-medium mb-0"&gt;
RFC 3986 section 2.5 specifies that a character is first converted to its UTF-8 byte sequence, then each byte is percent-encoded individually. The letter e-acute becomes the two bytes 0xC3 0xA9, which encode as %C3%A9. This is also why the legacy JavaScript escape function must never be used: it emits a non-standard %uXXXX form that is not valid percent-encoding.
&lt;/p&gt;
&lt;/details&gt;
&lt;/section&gt;</description></item></channel></rss>