URL Encoding Decoded: From Percent Signs to encodeURIComponent
URL Encoding Decoded: From Percent Signs to encodeURIComponent#
Last week, I was debugging a payment callback where URL parameters were double-encoded, causing order ID parsing failures. It turned out many developers still treat URL encoding as just “throw encodeURIComponent at it.” Let me break down the actual mechanics and implementation details.
The Essence: Percent-Encoding#
URL encoding is officially called “percent-encoding.” The rule is straightforward: non-ASCII and special characters are represented as %XX, where XX is the character’s hexadecimal ASCII value.
Original: https://example.com/search?q=hello world
Encoded: https://example.com/search?q=hello%20world
The space character (ASCII 32) becomes %20. For multi-byte UTF-8 characters, each byte gets its own %XX:
encodeURIComponent('你好')
// "%E4%BD%A0%E5%A5%BD"
// Each Chinese character is 3 bytes in UTF-8, so 6 %XX sequences
encodeURIComponent vs encodeURI: The Critical Difference#
JavaScript provides two encoding functions, and choosing the wrong one causes subtle bugs:
const url = 'https://example.com/path?name=John&age=25'
encodeURI(url)
// https://example.com/path?name=John&age=25
// Preserves ://, ?, & - the URL structure characters
encodeURIComponent(url)
// https%3A%2F%2Fexample.com%2Fpath%3Fname%3DJohn%26age%3D25
// Encodes EVERYTHING
The key difference: encodeURI preserves URL structural characters (://?&=;), used for encoding complete URLs. encodeURIComponent encodes all special characters, used for encoding parameter values.
Practical usage:
// ❌ Wrong: parameter value might contain & = breaking URL structure
const url = `https://api.com/search?q=${input}`
// ✅ Correct: only encode the parameter value
const url = `https://api.com/search?q=${encodeURIComponent(input)}`
// ✅ Correct: encoding a complete URL as a parameter
const redirect = encodeURIComponent('https://example.com/callback?code=123')
const loginUrl = `https://auth.com/login?redirect=${redirect}`
Reserved vs Unreserved Characters#
RFC 3986 defines two character categories:
Unreserved characters (no encoding needed):
A-Z a-z 0-9 - _ . ~
Reserved characters (have special meaning, may need encoding):
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
encodeURIComponent encodes all reserved characters except ! ' ( ) *. This design has historical reasons:
encodeURIComponent('hello world!')
// "hello%20world%21"
// Space becomes %20, ! becomes %21
// Some legacy systems encode space as +
'hello world'.replace(/ /g, '+')
// "hello+world"
// This is application/x-www-form-urlencoded, NOT URL encoding
Common Pitfalls#
1. Double Encoding#
const name = 'John Smith'
const encoded = encodeURIComponent(name) // "John%20Smith"
const doubleEncoded = encodeURIComponent(encoded) // "John%2520Smith"
// Decoding once gives the encoded string, not the original
decodeURIComponent(doubleEncoded) // "John%20Smith"
decodeURIComponent(decodeURIComponent(doubleEncoded)) // "John Smith"
This often happens when backend already encoded the value:
// Backend returns pre-encoded URL
const redirectUrl = 'https%3A%2F%2Fexample.com'
// Frontend encodes it again
const url = `https://api.com/callback?url=${encodeURIComponent(redirectUrl)}`
// Result: double-encoded URL
Solution - decode before encoding:
function safeEncode(str) {
try {
// Try decoding first to avoid double encoding
const decoded = decodeURIComponent(str)
return encodeURIComponent(decoded)
} catch {
// Decoding failed means it's original string, encode directly
return encodeURIComponent(str)
}
}
2. Handling Decode Failures#
decodeURIComponent throws on invalid encoding:
decodeURIComponent('%E0%A4%A') // URIError: URI malformed
// %E0%A4%A is not a valid UTF-8 sequence
Safe decoding implementation:
function safeDecode(str) {
try {
return decodeURIComponent(str)
} catch (e) {
console.error('URL decode failed:', e.message)
return str // Return original string
}
}
// Or replace invalid encodings with regex
function safeDecodeWithFallback(str) {
return str.replace(/%[0-9A-Fa-f]{2}/g, (match) => {
try {
return decodeURIComponent(match)
} catch {
return match // Keep invalid encoding
}
})
}
3. Inconsistent Space Encoding#
Different systems handle spaces differently:
// Standard URL encoding
encodeURIComponent('a b') // "a%20b"
// application/x-www-form-urlencoded (form submission)
const formEncoded = 'a b'.replace(/ /g, '+') // "a+b"
// Decoding difference
decodeURIComponent('a+b') // "a+b" (+ is NOT decoded to space)
decodeURIComponent('a%20b') // "a b"
For URL parameters, stick with %20:
function buildQueryString(params) {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
}
buildQueryString({ name: 'John Smith', city: 'New York' })
// "name=John%20Smith&city=New%20York"
Practical Application: Batch URL Parameter Processing#
When building tools, you often need to parse and construct URL parameters:
// Parse URL parameters
function parseUrlParams(url) {
const query = url.split('?')[1]
if (!query) return {}
return query.split('&').reduce((params, pair) => {
const [key, value] = pair.split('=')
try {
params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : ''
} catch (e) {
console.warn(`Failed to decode parameter: ${pair}`)
}
return params
}, {})
}
// Build URL with parameters
function buildUrl(baseUrl, params) {
const query = Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
.join('&')
return query ? `${baseUrl}?${query}` : baseUrl
}
// Usage
const url = 'https://api.com/search?q=hello&page=1'
const params = parseUrlParams(url)
// { q: 'hello', page: '1' }
const newUrl = buildUrl('https://api.com/search', { q: 'world', page: 2 })
// "https://api.com/search?q=world&page=2"
Performance: Handling Bulk Encoding#
For encoding many URL parameters, use native APIs:
// URLSearchParams (native in modern browsers)
const params = new URLSearchParams({
name: 'John Smith',
age: '25',
city: 'New York'
})
params.toString() // "name=John+Smith&age=25&city=New+York"
// Batch decode
const parsed = Object.fromEntries(params.entries())
// { name: 'John Smith', age: '25', city: 'New York' }
URLSearchParams handles encoding/decoding automatically and performs better than manual string concatenation.
Online Tool Implementation#
Based on these principles, I built an online URL Encoder/Decoder with these features:
- Auto-detect encode/decode mode
- Batch processing for multiple lines
- Friendly error messages (shows exactly which character failed)
- One-click swap input/output
The core is just encodeURIComponent and decodeURIComponent, but handling edge cases and errors properly makes it much more usable.
Related tools: JSON Formatter | Base64 Encoder/Decoder