Short answer: RFC 3986 writes a percent-encoded octet as %HH. Leave ASCII letters, digits, -, ., _, and ~ unescaped. Reserved characters may be delimiters or data, so encode them according to their component and contract. Convert non-ASCII text to UTF-8 bytes first, decode once at a defined boundary, and never repeatedly decode until a value “looks normal.”
Percent escapes represent octets
RFC 3986 section 2.1 defines the syntax pct-encoded = "%" HEXDIG HEXDIG. Each escape represents one octet, an eight-bit byte. The space byte is hexadecimal 20, so its representation is %20. A literal percent sign used as data is byte 25, so it becomes %25.
Non-ASCII text needs a character encoding before it has bytes. Modern web URL processing uses UTF-8. The word “café” ends with U+00E9, encoded by UTF-8 as bytes C3 A9, so a component encoder produces caf%C3%A9. The Korean syllable “한” is UTF-8 bytes ED 95 9C and becomes %ED%95%9C. One displayed character can therefore require several escapes.
space → 20 → %20
plus sign → 2B → %2B
é → C3 A9 → %C3%A9
한 → ED 95 9C → %ED%95%9C
😀 → F0 9F 98 80 → %F0%9F%98%80Hexadecimal letters are case-insensitive when parsed, but RFC 3986 recommends uppercase A through F for consistent output. That is why canonical examples usually show %2F instead of %2f.
The RFC 3986 unreserved set
Unreserved characters do not have a reserved delimiter purpose in generic URI syntax. The exact set is ALPHA / DIGIT / "-" / "." / "_" / "~". Encoding one does not change the resource under RFC syntax-based normalization, but producers should normally emit it literally. For example, %7Ealice and ~alice are equivalent at that level, yet not every application normalizes before comparing raw strings.
| Class | Characters | Normal producer behavior |
|---|---|---|
| Letters | A-Z a-z | Leave literal |
| Digits | 0-9 | Leave literal |
| Marks | - . _ ~ | Leave literal |
Do not confuse this RFC 3986 set with the characters JavaScript happens to leave unescaped. ECMAScript's URI functions retain a historical set based on RFC 2396, including !, *, single quote, and parentheses. The encodeURI versus encodeURIComponent guide explains that compatibility detail and when stricter component output may be required.
Reserved characters can be syntax or data
RFC 3986 divides reserved characters into general delimiters :/?#[]@ and sub-delimiters !$&'()*+,;=. Their meaning depends on position and the applicable scheme or application. A slash between path segments is intentional syntax. A slash inside one segment's data must generally be encoded as %2F if the receiving system permits an encoded slash at all.
The distinction matters because a literal reserved delimiter and its escaped byte are not generally equivalent. Changing ? to %3F can turn the start of a query into data inside a path. Changing & to %26 can turn a form field separator into part of a field value. Encode values before composition, then let the URL builder place separators.
/products/red/blue # slash separates path segments
/products/red%2Fblue # encoded slash is data if the server accepts it
?q=tea&sort=new # ampersand separates form-style query pairs
?q=tea%26coffee # encoded ampersand belongs to the q valueThere is no context-free safe character set
A scheme, host, user information field, path segment, whole path, query, query name, query value, and fragment have different parsing rules. Encoding an entire assembled URL as though it were one component hides structural separators. In the other direction, applying a whole-URI encoder to user data can leave &, =, #, or ? active and let the data alter structure.
In browser JavaScript, construct a complete URL with the URL API and add query pairs with URLSearchParams. Use a component encoder only when the surrounding protocol calls for that precise representation. Parsing with the LiveParse URL Parser can show where the browser places the scheme, authority, path, query, and fragment; it does not visit or fetch the parsed address.
A plus sign is not a universal space escape
In generic RFC 3986 syntax, plus is a reserved sub-delimiter and remains a plus. The special space conversion belongs to application/x-www-form-urlencoded. The WHATWG form serializer emits a space as +, while its parser changes a raw + to a space before percent-decoding. A literal plus in such a field must be represented as %2B.
This is why C++ becomes C%2B%2B in a form-style query value. Replacing every plus in a full URL with a space is wrong: a path such as /docs/a+b can use a literal plus. See %20 versus + for a complete comparison.
Validate and decode exactly one round
A strict component decoder should reject a stray percent sign, a non-hex pair such as %G0, a truncated escape, and byte sequences that are not valid UTF-8 for the expected text contract. Decoding once turns %252F into %2F, not into /. The remaining escape may be intentional data from an outer layer.
RFC 3986 explicitly warns against encoding or decoding the same string more than once. Repeated decoding can change syntax after an earlier validation step and is a common ingredient in path traversal, routing, and filter-bypass bugs. The double URL encoding guide shows why each layer needs one owned boundary.
Use the browser tools within their stated boundary
The URL Encoder and URL Decoder work locally in the browser. The Query String Parser applies query-pair rules, while the URL Parser separates a complete URL and can resolve a relative reference against an explicit base. These tools do not send entered values to an encoding service and do not fetch the destination.
General text and component input is limited to 200,000 UTF-16 code units. Complete URL and base URL fields are limited to 32,768 code units. These are responsiveness boundaries for an interactive page, not a statement that downstream servers, browsers, proxies, or frameworks accept URLs of that size. Do not paste secrets from production URLs unless local handling is appropriate for your environment.
Primary specifications
- RFC 3986 section 2 — percent-encoding, reserved and unreserved characters, and when to encode or decode.
- WHATWG URL Standard — browser URL parsing, percent-encode sets, and UTF-8 processing.
- ECMAScript URI handling functions — exact JavaScript encoding and decoding algorithms.
Need to inspect an encoded value? Decode one deliberate round, keep the original visible for comparison, and verify whether the input is a complete URL, a component, or form-style query data.
Open the URL DecoderFrequently asked questions
What is URL percent-encoding?
Percent-encoding represents one byte as a percent sign followed by two hexadecimal digits. Non-ASCII text is normally converted to UTF-8 first, so one Unicode character can produce several consecutive percent escapes.
Which characters are unreserved in RFC 3986?
RFC 3986 defines ASCII letters, digits, hyphen, period, underscore, and tilde as unreserved. Producers should normally leave them literal because encoding an unreserved character adds no delimiter protection.
Should every reserved character be percent-encoded?
No. Keep a reserved character literal when it intentionally acts as syntax, such as a slash between path segments or an ampersand between form fields. Percent-encode it when it is data that would otherwise conflict with that role.
Why can one Unicode character become several percent escapes?
Percent escapes represent bytes, not characters. UTF-8 uses multiple bytes for many Unicode code points, and each byte receives its own percent sign and two hexadecimal digits.
Does a plus sign always mean a space in a URL?
No. A plus sign is literal in the general URI syntax. It is converted to a space by the application/x-www-form-urlencoded parser used by URLSearchParams and many form-style query parsers.
Does LiveParse upload text entered into its URL tools?
No. Encoding, decoding, URL parsing, and query parsing run in the browser without sending the entered value to a processing API or fetching the destination URL. General text input is capped at 200,000 UTF-16 code units, and URL or base fields are capped at 32,768.