Developer Tools

URL Encoding UTF-8 and International Characters Correctly

Master URL encoding for international characters, emojis, and UTF-8 text. Learn how café becomes caf%C3%A9 and why proper Unicode handling prevents broken links.

By Inventive HQ Team

To encode an international character in a URL, you convert it to its UTF-8 bytes and then write each byte as a percent sign followed by two hexadecimal digits (%HH). The character é is Unicode code point U+00E9; in UTF-8 that is the two bytes C3 A9; percent-encoding each byte produces %C3%A9, so café travels across the wire as caf%C3%A9. The rule is the same for every non-ASCII character — an emoji like 🎉 (four UTF-8 bytes F0 9F 8E 89) becomes %F0%9F%8E%89. The URL never carries the raw character; it carries the percent-encoded bytes.

That is the summary an AI overview gives you. The part it can't give you is the mechanics: exactly how a character turns into bytes, why é is %C3%A9 and not %E9, how many bytes each script costs you, and where hand-rolled encoding quietly corrupts your data. If you want the broader "how do I support international users" picture — Punycode domains, IDNs, and internationalization strategy — read the companion piece, How do I encode international characters in URLs?. This post drills into the byte-level pipeline for the path and query string.

The pipeline: character → UTF-8 bytes → percent-encoding

Every non-ASCII character in a URL goes through the same two-stage conversion. First UTF-8 turns the character into a sequence of bytes. Then percent-encoding turns each byte into an ASCII-safe %HH token.

UTF-8 percent-encoding pipeline for the character é The character é becomes the UTF-8 bytes C3 A9, which are then percent-encoded to %C3%A9, shown as a left-to-right flow. One character, two conversions é U+00E9 character UTF-8 C3 A9 two UTF-8 bytes %HH %C3%A9 URL-safe

The critical detail people get wrong is stage one. The code point of é is U+00E9, decimal 233, which fits in a single byte — so it is tempting to write %E9. That is Latin-1 encoding, not UTF-8. In UTF-8, every character above U+007F is stored as multiple bytes, and U+00E9 becomes C3 A9. Encode the bytes UTF-8 produces, never the raw code-point value.

Advertisement

The lookup table: characters, code points, bytes, and encodings

Here is the full pipeline for a representative character from each UTF-8 length class. The "Bytes" column is what UTF-8 produces; the "Percent-encoded" column is what actually travels in the URL.

CharacterNameCode pointUTF-8 bytesByte countPercent-encoded
aLatin small aU+0061611a (unreserved — not encoded)
ée with acuteU+00E9C3 A92%C3%A9
ñn with tildeU+00F1C3 B12%C3%B1
Euro signU+20ACE2 82 AC3%E2%82%AC
CJK "you"U+4F60E4 BD A03%E4%BD%A0
🎉Party popperU+1F389F0 9F 8E 894%F0%9F%8E%89

Three things to read out of this table:

  • ASCII passes through untouched. Unreserved characters (A–Z, a–z, 0–9, and - _ . ~) are one byte and never need encoding. encodeURIComponent('a') returns a.
  • Byte count scales with the script. European accents cost 2 bytes, most CJK and symbols cost 3, emojis cost 4. A single emoji expands to twelve URL characters (%F0%9F%8E%89), which matters when you have URL length limits.
  • The percent-encoded form is just the bytes with % in front of each. There is no arithmetic — once you have the UTF-8 bytes, encoding is a mechanical rewrite.

Do it with a library, never by hand

Every mainstream language ships a correct UTF-8 URL encoder. Use it — the surrogate-pair and multi-byte edge cases are exactly where hand-written string replacement breaks.

Language / contextEncode a valueResult for café
JavaScriptencodeURIComponent('café')caf%C3%A9
Pythonurllib.parse.quote('café')caf%C3%A9
PHPrawurlencode('café')caf%C3%A9
JavaURLEncoder.encode("café", "UTF-8")caf%C3%A9 (spaces become +)
PowerShell[System.Uri]::EscapeDataString('café')caf%C3%A9
Shell (via Python)python3 -c 'import sys,urllib.parse;print(urllib.parse.quote("café"))'caf%C3%A9

Want to see the byte-level breakdown for any string interactively? The tool below encodes and decodes UTF-8 URLs entirely in your browser and shows exactly which bytes each character expands to.

Loading interactive tool...

encodeURI vs encodeURIComponent: pick the right scope

Both functions produce identical UTF-8 percent-encoding for international characters. They differ only in which reserved (structural) characters they leave alone:

  • encodeURIComponent encodes the reserved set (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) too. Use it for a single piece of data — one query value, one path segment — so the value cannot break out of its slot. This is the one you want 95% of the time.
  • encodeURI preserves those reserved characters so an already-assembled URL keeps its structure. Use it only when encoding a complete URL that must remain a working URL.

The consequence: encodeURIComponent('a/b?c=d') gives a%2Fb%3Fc%3Dd (safe as a value), while encodeURI('a/b?c=d') gives a/b?c=d (structure preserved). For international characters specifically, they behave identically — the difference is entirely about reserved ASCII punctuation. For the path-versus-query reserved-character rules, see handling special characters in URL paths vs query strings, and for how a URL breaks into encodable pieces, see URL encoding components explained.

Mojibake: the #1 international-encoding bug

Mojibake — garbled text like café rendering as café — happens when one layer encodes as UTF-8 and another decodes as Latin-1. The two UTF-8 bytes C3 A9 get read as two separate Latin-1 characters (à and ©). The failure is never in the URL encoding itself; it is a character-set disagreement between layers.

The fix is to force UTF-8 at every stage:

  • Send Content-Type: text/html; charset=utf-8 (and the equivalent on JSON/form responses).
  • Configure your database connection and columns as utf8mb4 (MySQL's real UTF-8), not latin1.
  • In Python, decode bytes explicitly with .decode('utf-8') rather than trusting the platform default.
  • When you see %E9 in a legacy URL where you expected %C3%A9, you are looking at Latin-1 data — decode it as Latin-1, then re-encode as UTF-8.

Encoding is not security

URL encoding keeps data syntactically inside its parameter — it is not a security control. Two traps:

  • Double-encoding bypass. A filter blocking ../ (directory traversal) can miss %2e%2e%2f, and a system that decodes twice can turn %252e%252e back into ../ after passing the filter. Always validate after fully decoding, and reject input that decodes to something dangerous.
  • Wrong-context escaping. A value that is safe in a URL is not automatically safe in HTML. If you reflect a decoded query parameter into a page, you still need HTML-escaping to prevent cross-site scripting (XSS). Encode for the destination, not just the transport.

Percent-encoding solves correctness. Validation and context-appropriate escaping solve security. You need both.

Key takeaways

  • International URL encoding is always two steps: character → UTF-8 bytes → %HH per byte.
  • Encode the UTF-8 bytes, not the code point — that is why é is %C3%A9, not %E9.
  • Byte count is variable: 1 byte for ASCII, 2 for European accents, 3 for most CJK and symbols, 4 for emojis.
  • Use encodeURIComponent (or your language's equivalent) for values; never hand-roll it.
  • Force UTF-8 at every layer to avoid mojibake, and never treat encoding as a security control.

Ready to encode or decode UTF-8 URLs and see the byte breakdown? Use our URL Encoder/Decoder tool for instant, accurate, client-side encoding with automatic format detection.

Frequently Asked Questions

How does UTF-8 URL encoding actually work?

It is a two-step pipeline. First, the character is converted to its UTF-8 byte sequence — one to four bytes depending on the character. Second, each of those bytes is written as a percent sign followed by its two-digit hexadecimal value (%HH). The letter é is code point U+00E9, its UTF-8 bytes are C3 A9, so its percent-encoded form is %C3%A9. The URL never carries the raw character; it carries the ASCII-safe %HH representation of each byte.

Why is é encoded as %C3%A9 and not %E9?

Because you encode the UTF-8 bytes, not the Unicode code point. The code point of é is U+00E9 (decimal 233), but in UTF-8 any character above U+007F is stored as multiple bytes. U+00E9 becomes the two bytes C3 A9, and percent-encoding each byte gives %C3%A9. The older form %E9 is Latin-1 (ISO-8859-1) encoding of the same character — mixing it with a UTF-8 decoder produces mojibake. Modern URLs are UTF-8, so %C3%A9 is correct.

How many bytes does a character take in UTF-8?

UTF-8 is variable-length. ASCII characters (U+0000 to U+007F, such as A-Z, 0-9) take one byte. Latin accented letters and most European scripts (U+0080 to U+07FF, e.g. é, ñ, ü) take two bytes. The rest of the Basic Multilingual Plane, including CJK characters like 你 and most symbols such as €, take three bytes. Emojis and other supplementary-plane characters (above U+FFFF) take four bytes. Each byte becomes one %HH sequence, so one emoji expands to twelve URL characters.

How do I encode an emoji in a URL?

Encode its UTF-8 bytes. The party popper 🎉 is code point U+1F389, which UTF-8 stores as the four bytes F0 9F 8E 89, giving the percent-encoded string %F0%9F%8E%89. In JavaScript, encodeURIComponent('🎉') returns exactly that. Never try to hand-encode an emoji from its code point — you must go through the UTF-8 byte sequence, and the surrogate-pair mechanics for supplementary characters are easy to get wrong by hand.

What is the difference between encodeURI and encodeURIComponent?

Both produce UTF-8 percent-encoding, but they differ in which characters they leave alone. encodeURI is meant for a whole URL and preserves the reserved characters that give a URL its structure (: / ? # & =), so it will not break the address apart. encodeURIComponent is meant for a single piece of data — one query value or one path segment — and encodes those reserved characters too, so the data cannot escape its slot. Use encodeURIComponent for values you drop into a query string; use encodeURI only when encoding an already-assembled URL.

Does the path and the query string get encoded the same way?

The UTF-8-to-percent-encoding step is identical in both, but the set of reserved characters differs. In the path, characters like / and : have structural meaning; in the query string, & = ? do. A space becomes %20 in a path but is often written as + in a query string under application/x-www-form-urlencoded. For international characters specifically, the rule is the same everywhere: turn the character into UTF-8 bytes and percent-encode each byte. See our companion guide on paths versus query strings for the reserved-character details.

Why do my accented characters turn into garbled symbols (mojibake)?

Mojibake happens when a string is encoded with one character set and decoded with another. The classic case is text encoded as UTF-8 but decoded as Latin-1: café becomes café, because the two UTF-8 bytes C3 A9 are read as two separate Latin-1 characters (à and ©). The fix is to make sure every layer — the encoder, the HTTP headers, and the decoder — agrees on UTF-8. Always specify UTF-8 explicitly rather than relying on a system default.

Is URL encoding enough to make user input safe?

No. URL encoding is for syntactic correctness — keeping data inside its parameter slot — not for security. It does not stop XSS, SQL injection, or path traversal on its own. Double-encoding attacks (%252e%252e for ../) can slip past naive filters, and a value that is safe in a URL can still be dangerous once decoded and placed into HTML. Always validate and escape input for the destination context (HTML-escape for HTML output, parameterize for SQL) in addition to URL encoding.

What tools can encode UTF-8 URLs from the command line?

On Linux or macOS, pipe through Python: echo -n 'café' | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read()))' yields caf%C3%A9. In a browser console, encodeURIComponent('café') does the same. In PowerShell, [System.Uri]::EscapeDataString('café') works. All of these default to UTF-8, which is what modern URLs require.

url encodingpercent encodingweb developmentUTF-8security