URL encoding diagnostics · Updated October 31, 2026
URL Encode Unicode: Same-Looking Text, Different Bytes
URL encoding does not automatically normalize Unicode. A precomposed é encodes as %C3%A9; an e followed by a combining acute accent encodes as e%CC%81. Compare the strings below before changing them. NFC can make canonically equivalent text match, but your application must choose that policy.
Use complete quoted JSON strings. Escapes make invisible code-point differences reproducible. Processing stays in this browser; this checker previews normalized alternatives and does not replace either input.
Locate the difference before normalizing it
- Capture each original JavaScript value with
JSON.stringify(value). Keep that raw value alongside its encoded spelling. - Paste both quoted strings in the comparator. Compare code points first, then the raw percent-encoded output.
- Inspect the NFC and NFKC previews separately. A match after NFC and a match only after NFKC describe different transformations.
- Decide the application rule before serializing a query value. If exact text matters, keep the original; if the producer and consumer agree on NFC, normalize the value before encoding it.
- Retest storage, lookup and any byte-sensitive comparisons with the original pair. A client-side preview cannot establish how a particular server treats the two requests.
Eight pairs, with independently checked encoded output
We ran these fixed pairs on October 31, 2026 using Node v24.19.0. A separate Python 3.13.15 run checked both sides in three modes: raw, NFC and NFKC. All 48 encoded outputs matched for these fixtures. Python used urllib.parse.quote with the safe characters explicitly matched to encodeURIComponent; its default slash policy would not be the same comparison. The Node and Python Unicode data versions are recorded as 17.0 and 15.1.0 respectively. These examples are not a test of every Unicode character or version combination.
| Pair | Code points: left / right | Raw URL encoding: left / right | Equal after NFC? | Equal after NFKC? |
|---|---|---|---|---|
accent | U+0063 U+0061 U+0066 U+00E9 / U+0063 U+0061 U+0066 U+0065 U+0301 | caf%C3%A9 / cafe%CC%81 | true | true |
angstrom | U+00C5 / U+0041 U+030A | %C3%85 / A%CC%8A | true | true |
hangul | U+AC00 / U+1100 U+1161 | %EA%B0%80 / %E1%84%80%E1%85%A1 | true | true |
fullwidth | U+FF21 / U+0041 | %EF%BC%A1 / A | false | true |
ligature | U+FB00 / U+0066 U+0066 | %EF%AC%80 / ff | false | true |
roman | U+2163 / U+0049 U+0056 | %E2%85%A3 / IV | false | true |
nbsp | U+0061 U+00A0 U+0062 / U+0061 U+0020 U+0062 | a%C2%A0b / a%20b | false | true |
case | U+0041 / U+0061 | A / a | false | false |
Three different failure paths in this matrix
A composed character and a decomposed sequence: the accent pair has different original code points and different encoded values, even if your font draws them alike. NFC makes their values match. The Hangul pair reaches the same result through a syllable versus a Jamo sequence, so this is not limited to adding accents to Latin letters.
Compatibility folding is a wider choice: NFC leaves fullwidth A and ordinary A distinct; NFKC makes them match. The ligature and Roman-numeral pairs also match only after NFKC. That can be useful for a deliberately forgiving search field. It also removes a distinction that an exact identifier may need. Do not call NFKC a harmless URL repair.
An ordinary-looking space: the nbsp pair contains U+00A0 on one side and U+0020 on the other. Their raw encodings are a%C2%A0b and a%20b. NFC preserves the difference; NFKC collapses it. This is a different problem from choosing plus versus %20 for form serialization. Changing the serializer alone does not turn a non-breaking space into an ordinary space.
The case control stays unequal after both NFC and NFKC. Unicode normalization is not case folding. These controls keep an apparent fix from becoming a rule that promises every similar-looking input will match.
Decoding successfully does not prove equivalence
Our regression asserts that each encoded value decodes back to the exact string supplied to that encoding step. The raw pair can therefore pass a round-trip test on both sides while still producing different bytes. The normalized output round-trips to the normalized string, not necessarily to the original one. Keep those assertions separate.
Percent-encoding normalization is another layer. RFC 3986 discusses normalizing escape spellings and unreserved characters when comparing URIs; that does not grant permission to normalize every application identifier. Work on the intended component value. Applying a text cleanup to a complete URL can mix that decision with separators, path structure and query parsing.
Rerun the comparison
Download the Node regression script, eight fixture pairs and recorded output. Put them in downloads/ and place the shared comparison core in assets/ under the same parent folder. Then run:
node downloads/url-unicode-normalization-regression.cjs
The script checks expected equality in each mode and each encoded round trip; a failed assertion exits nonzero. It also checks that a lone surrogate is rejected. For that separate failure, use the URIError and lone-surrogate checker. All pairs here contain valid Unicode text; they test identity differences rather than malformed UTF-16.
Should I normalize all URLs to NFC?
No blanket rule follows from these fixtures. Agree on the normalization of the specific value with its producer and consumer, and test the relevant lookup. Do not rewrite an opaque or already issued identifier just because two previews match.
Does a URL encoder choose NFC for me?
The direct JavaScript encoding operation tested here preserves the distinction. Normalization is an explicit operation in the comparison. For normal component escaping, return to the URL encoder; for API syntax, see the JavaScript encoding guide.
References checked October 31, 2026: String.normalize forms and examples; RFC 3986 percent-encoding normalization. The eight-pair matrix and cross-runtime output checks are reproducible project experiments, not a claim that normalization itself is an exclusive discovery.