URL decoding looks simple until a production bug proves otherwise. Someone copies a callback URL from logs, a query value contains %26, a search term arrives with plus signs, or a redirect parameter is decoded twice and suddenly points somewhere else. The operation is small. The context around it is where the bugs live.
This guide is for developers debugging API calls, OAuth redirects, tracking links, webhooks, analytics URLs, search forms, and pasted log snippets. The goal is not just to turn %20 into a space. The goal is to decode the right URL part exactly once, with the right rules for that part.
Quick answer
URL decoding converts percent-encoded byte sequences back to characters. In JavaScript, use URLSearchParams for query strings, decodeURIComponent for a single encoded value, and decodeURI only for a whole URL. In Python, use urllib.parse.unquote for percent encoding and unquote_plus for form-style query values where + means space.
What URL decoding actually decodes
Percent-encoding represents bytes using a percent sign followed by two hexadecimal digits. A decoded character may come from one byte, such as %2F for slash, or several bytes, such as %E2%9C%93 for a check mark in UTF-8. Modern web URLs should be treated as UTF-8 unless a legacy system explicitly documents another encoding.
| Encoded | Decoded | Why it matters |
|---|---|---|
| %20 | space | Most URL contexts encode a space as %20. |
| + | space in form query strings | Only application/x-www-form-urlencoded treats plus as space. |
| %2F | / | A slash inside data, not necessarily a path separator. |
| %3F | ? | A literal question mark inside a value. |
| %26 | & | A literal ampersand inside a query value. |
| %3D | = | A literal equals sign inside a query value. |
| %25 | % | The percent sign itself, often seen in double-encoding bugs. |
| %E2%9C%93 | check mark | A UTF-8 character encoded as three bytes. |
The important detail is that decoding can create structural URL characters. If %26 becomes & before you parse the query string, one value can accidentally become two parameters. That is why order matters: parse structure first when you have a full URL; decode values after the structure is known.
decodeURI vs decodeURIComponent vs URLSearchParams
JavaScript gives you more than one decoder because different URL parts reserve different characters. The shortest rule is: decodeURIComponent decodes data, decodeURI decodes a URL while keeping its separators meaningful, and URLSearchParams should be your default for query strings.
| Function | Best for | Keeps URL structure? | Plus-space rule |
|---|---|---|---|
| decodeURIComponent(value) | One query value, one form field, one path segment. | No | Does not turn + into space. |
| decodeURI(url) | A whole URL that is already structurally valid. | Yes | Does not turn + into space. |
| new URL(url).searchParams | Reading query parameters in browsers or Node.js. | Parses structure first | Turns + into space for query strings. |
| urllib.parse.unquote(value) | Python decoding for path segments or percent-encoded values. | No | Does not turn + into space. |
| urllib.parse.unquote_plus(value) | Python decoding for form-style query values. | No | Turns + into space. |
const raw = "https://example.com/search?q=red%20shoes%20%26%20socks&next=%2Fcheckout%3Fstep%3D1";
const url = new URL(raw);
console.log(url.searchParams.get("q"));
// red shoes & socks
console.log(url.searchParams.get("next"));
// /checkout?step=1
console.log(decodeURI(raw));
// https://example.com/search?q=red shoes %26 socks&next=%2Fcheckout%3Fstep%3D1
console.log(decodeURIComponent("red%20shoes%20%26%20socks"));
// red shoes & socksIn real code, I rarely decode a full URL string manually. I construct a URL, read the exact field, and let the platform parser preserve the boundary between path, query, and fragment. It is less dramatic than string splitting, and that is the point.
Runnable JavaScript: decode query values safely
This Node.js example shows the safest default workflow for a full URL: parse first, then read values. It also shows the common mistake of decoding the whole query text before splitting on &.
// decode-url.js
const rawUrl =
"https://api.example.test/items?q=red%20shoes%20%26%20socks&tag=a%2Fb&debug=true";
const url = new URL(rawUrl);
console.log("q:", url.searchParams.get("q"));
console.log("tag:", url.searchParams.get("tag"));
console.log("debug:", url.searchParams.get("debug"));
// Bad idea: decoding the whole query can create new delimiters.
const rawQuery = rawUrl.split("?")[1];
console.log("decoded raw query:", decodeURIComponent(rawQuery));
// Run:
// node decode-url.js
// Expected output:
// q: red shoes & socks
// tag: a/b
// debug: true
// decoded raw query: q=red shoes & socks&tag=a/b&debug=trueThe decoded raw query output is misleading. It visually suggests that socks might be a separate parameter because %26 has already become &. That is fine for display, but not for parsing. When parsing, keep delimiters encoded until the URL parser has done its job.
Runnable Python: unquote vs unquote_plus
Python has the same distinction, but it is named differently. Use unquote when decoding ordinary percent-encoded text. Use unquote_plus for form-encoded query values where plus is a space.
# decode_url.py
from urllib.parse import parse_qs, unquote, unquote_plus, urlparse
raw_url = "https://example.test/search?q=red+shoes%2Bsocks&path=a%2Fb"
parsed = urlparse(raw_url)
params = parse_qs(parsed.query)
print(params["q"][0])
print(params["path"][0])
print(unquote("red+shoes%2Bsocks"))
print(unquote_plus("red+shoes%2Bsocks"))
# Run:
# python decode_url.py
# Expected output:
# red shoes+socks
# a/b
# red+shoes+socks
# red shoes+socksThe subtle part is %2B. It decodes to a literal plus. A raw + in a form query decodes to a space. When search terms, math expressions, email aliases, or phone numbers contain plus signs, you need to know whether the plus was raw or encoded.
Common URL decoding mistakes
Most URL decode bugs are not caused by the decoder being broken. They happen because code decodes at the wrong layer. Treat decoding as a boundary decision, not a random cleanup step.
Decoding the whole URL when only one value should be decoded
If you decode an entire URL, encoded delimiters such as %26 and %3D can become real separators. Parse the URL first, then decode the field you need.
Double decoding
A value like %252F becomes %2F after one decode and / after the second. That can change an ID into a path delimiter or a harmless string into a redirect target.
Using the wrong space rule
A plus sign is a space in form-encoded query strings, but it is a literal plus in many other contexts. This is the source of many broken search and OAuth callback bugs.
Treating malformed percent escapes as normal text
Inputs like %E0%A4%A or %ZZ are not valid encoded data. JavaScript throws URIError; Python may replace invalid bytes depending on the error handling you choose.
Decoding before security checks
Access-control, redirect, and path checks should be explicit about whether they inspect raw text or decoded text. Mixing the two is how bypasses happen.
Double decoding: the bug that hides in logs
Double decoding is especially easy to miss because logs, proxies, frameworks, and browsers may already decode some parts for display. If you decode again in application code, the value can change meaning. Here is the small example that I use when reviewing redirect or file-path code:
const value = "%252Fadmin%252Fsettings"; const once = decodeURIComponent(value); const twice = decodeURIComponent(once); console.log(once); // %2Fadmin%2Fsettings console.log(twice); // /admin/settings
After one decode, the value is still text that contains encoded slashes. After two decodes, it is a path. That difference matters for redirects, file downloads, object keys, cache keys, route matching, and authorization checks. The safest practice is to decode once as close as possible to the request boundary, store that boundary choice in a variable name, and avoid helper functions that silently decode again.
Production habit
In request handlers, name values by state: rawNext, decodedNext, validatedNextPath. That sounds fussy, but it prevents the same string from being decoded, validated, and mutated in different places.
Malformed percent escapes and UTF-8 errors
Not every percent sign starts valid encoded data. JavaScript is strict: decodeURIComponent throws URIError for malformed sequences. That is a useful signal when a URL came from user input, logs, old email clients, or manual copy-paste.
Throws
decodeURIComponent("%E0%A4%A");
// URIError: URI malformed
decodeURIComponent("%ZZ");
// URIError: URI malformedHandled
function tryDecode(value) {
try {
return { ok: true, value: decodeURIComponent(value) };
} catch {
return { ok: false, value };
}
}Do not fix malformed input by repeatedly replacing percent signs until the exception disappears. That can turn invalid data into different valid data. Decide whether the correct behavior is to reject the request, display the raw value, or decode with a documented replacement strategy.
When not to decode a URL
There are times when decoding makes debugging easier but the application less correct. The raw encoded form is sometimes the data you need to preserve.
Signed URLs
Changing encoding can invalidate a signature because the byte-for-byte canonical string changes.
Cache keys
Raw and decoded forms may map differently. Normalize intentionally, not by accident.
Redirect parameters
Decode once, validate the destination, and reject cross-origin or protocol-relative surprises.
Object storage keys
A slash may be data inside an object key, not a folder separator.
Debugging checklist
- Identify whether you have a full URL, a query string, one query value, a path segment, or a form body.
- Parse the full URL before manually decoding values.
- Use the decoder that matches the context: component, whole URL, or form query.
- Check for double decoding by testing values containing
%252F,%2526, and%253D. - Reject, preserve, or safely display malformed input instead of forcing it through a decoder.
Frequently asked questions
What does URL decode mean?
URL decoding converts percent-encoded byte sequences such as %20, %2F, %3F, and UTF-8 sequences back into readable characters.
Should I use decodeURI or decodeURIComponent?
Use decodeURIComponent for one encoded value. Use decodeURI only when decoding a full URL while preserving URL structure.
Why does plus become a space in some decoded URLs?
A plus sign means space in form-encoded query strings and form bodies. In other URL contexts, plus can be a literal plus. Use URLSearchParams or unquote_plus when you are intentionally reading form-style query values.
What causes URIError: malformed URI sequence?
It usually means the input has invalid percent escapes or invalid UTF-8 after percent decoding. Examples include %ZZ and incomplete multi-byte sequences.
Is double decoding dangerous?
Yes. Double decoding can turn safe-looking text into delimiters, paths, or redirect targets. Decode once at the boundary and validate the decoded value before using it.
Try These Tools
URL Decode
Decode percent-encoded strings and inspect query parameters quickly.
Open toolURL Encode
Encode unsafe characters for query strings, path segments, and API requests.
Open toolBase64 Decode
Decode Base64 payloads that often appear inside URL parameters and tokens.
Open toolcURL Converter
Convert cURL requests after you have fixed encoded query parameters.
Open toolAuthoritative references
Use primary references when behavior differs across platforms:
- WHATWG URL Standard for the modern parsing model used by browsers.
- MDN decodeURIComponent documentation for JavaScript component decoding and malformed input behavior.
- MDN URLSearchParams documentation for query-string parsing in browser and Node-compatible runtimes.
- Python urllib.parse documentation for
unquote,unquote_plus, and URL parsing functions.
Related tutorials
URL Encoding Guide
Learn how to encode URL parts before sending API requests or form data.
HTTP API Best Practices
Design request URLs, errors, and status codes that are easier to debug.
API Integration Examples
Practical fetch patterns for query strings, retries, and API errors.
Base64 Decode Errors
Fix malformed Base64 values that often travel inside URL parameters.
Written by Zhisan
Independent Developer - Last updated July 2026