Quick Answer

URL encoding, officially called percent-encoding, replaces unsafe characters in a URL with a percent sign followed by two hexadecimal digits. A space becomes %20. It matters because URLs reserve certain characters for structure, so any user data containing those characters must be encoded or the URL breaks.

URL encoding is the process of replacing characters that aren't safe in a URL with a percent sign followed by two hexadecimal digits, which is why the official name is percent-encoding. A space becomes %20, an ampersand becomes %26, and a question mark becomes %3F. It matters because a URL isn't just a string. It's a structured address where characters like ?, &, / and # carry meaning, and the moment user data contains one of them, something breaks. The rules come from RFC 3986, published by the IETF in January 2005 as Internet Standard 66.

Encode or Decode a URL Now

Our free URL encoder and decoder handles both directions in your browser. Nothing is uploaded, so query strings with real data stay on your device.

Open URL Encoder →

What is URL encoding?

Percent-encoding is a substitution scheme. You take a character, look up the byte value of that character, write it in hexadecimal, and stick a percent sign in front. MDN Web Docs describes it as a mechanism to encode 8-bit characters that have specific meaning in the context of URLs.

So http://example.com/my file.txt becomes http://example.com/my%20file.txt. The space has a byte value of 32, which is 20 in hexadecimal, giving you %20.

That's the whole idea. What makes it trip people up is knowing which characters to encode and where in the URL to encode them, because the answer changes depending on which part of the URL you're touching.

Advertisement

Why does URL encoding exist at all?

Because a URL has to be parsed, and parsing needs delimiters. When a browser sees https://example.com/search?q=cats&page=2, it uses the ? to find where the query starts, and the & to split parameters apart. Those characters are structure, not content.

Now imagine someone searches for cats & dogs. Drop that raw into the URL and you get ?q=cats & dogs&page=2. The parser sees a parameter called q with the value cats , then a parameter called dogs with no value, then page=2. Your search is now broken and you didn't write a single bug. Encode it to ?q=cats%20%26%20dogs&page=2 and it works, because the encoded ampersand is data rather than a delimiter.

The same problem shows up everywhere user input meets a URL. Redirect targets, API tokens, filenames, email addresses in a query string, tracking parameters. If you build campaign links, our UTM parameters guide covers the same trap from the marketing side.

Which characters actually need encoding?

RFC 3986 splits the character space into two named groups, and getting familiar with both saves a lot of guessing.

Unreserved characters are always safe and should never be encoded. Section 2.3 defines them as ALPHA / DIGIT / "-" / "." / "_" / "~", which works out to exactly 66 characters: 26 uppercase letters, 26 lowercase letters, 10 digits, and 4 punctuation marks. Encoding these is technically legal but pointless, and it makes URLs uglier and longer for no gain.

Reserved characters are the delimiters. RFC 3986 breaks them into two sets in Section 2.2:

SetCharactersCount
gen-delims: / ? # [ ] @7
sub-delims! $ & ' ( ) * + , ; =11

That's 18 reserved characters total. Each one needs encoding when it appears as data rather than as structure. A slash separating path segments stays a slash. A slash inside a filename becomes %2F.

Everything outside those two groups gets encoded too. That covers spaces, quotes, angle brackets, control characters, and every non-ASCII character. Accented letters, Chinese characters, and emoji all get converted to UTF-8 bytes first, then each byte becomes its own percent-escape. That's why a single emoji often turns into four escapes.

How does percent-encoding work under the hood?

The grammar in RFC 3986 is one line: pct-encoded = "%" HEXDIG HEXDIG. Always a percent sign, always exactly two hex digits. Three steps get you there.

  • Convert the character to bytes using UTF-8.
  • Write each byte as a two-digit hexadecimal number.
  • Prefix each with a percent sign.

One detail people miss: case. The spec treats %3A and %3a as equivalent, but it also states that producers and normalizers should use uppercase hexadecimal digits for all percent-encodings. Most libraries follow this. If you're comparing URLs for equality or generating cache keys, normalise the case first or you'll get false mismatches on identical URLs.

Case is only half of that problem, though. The other half catches people who think two different-looking URLs must point at different things.

They often don't. RFC 3986 devotes a whole section to normalisation, and the rule that bites hardest is in Section 6.2.2.2: "percent-encoded octets corresponding to characters in the unreserved set should be decoded to their corresponding unreserved characters by URI normalizers." Unreserved means ALPHA / DIGIT / "-" / "." / "_" / "~", so %7E and ~ are the same character wearing different clothes.

Which means http://example.com/%7Euser and http://example.com/~user are the same URI. Encoding an unreserved character isn't wrong exactly, it's just noise, and a correct normaliser will strip it back out.

The spec is also clear about why anyone should care, and the list is longer than you'd guess. Comparison happens "every time a response cache is accessed, a browser checks its history to color a link, or an XML parser processes tags within a namespace." So the places this leaks are the places nobody thinks to look:

  • Cache keys. Two spellings of one URL become two cache entries, and your hit rate quietly drops.
  • Deduplication. Crawlers, analytics and job queues all end up processing the same resource twice.
  • Request signing. AWS Signature V4 and OAuth 1.0a both build a signature over a canonical URI. If the signer and the verifier normalise differently, every request fails authentication and the error message tells you nothing useful.
  • Allow-lists. Comparing a raw string against a list of permitted paths misses the percent-encoded spelling entirely, which is the same class of bug as the parser-confusion CVEs further down this page.

The habit that avoids all four: normalise before you compare, never compare raw strings. Uppercase the hex digits, decode anything unreserved, then check equality on the result.

Encoding is fully reversible and completely public. There's no key and no secret. It's the same relationship Base64 has with encryption, which is to say none at all.

Which JavaScript function should you use?

JavaScript ships two encoders and picking the wrong one is probably the single most common URL bug in web development.

According to MDN, encodeURI() leaves 82 characters unescaped: the 62 alphanumerics, the 9 marks - _ . ! ~ * ' ( ), and the 11 reserved characters ; / ? : @ & = + $ , #. Meanwhile encodeURIComponent() leaves only 71 unescaped, because it encodes those 11 reserved characters too.

FunctionLeaves unescapedUse it for
encodeURI()82 charactersA complete URL that still needs its structure intact
encodeURIComponent()71 charactersA single query value, path segment, or fragment

The practical rule is short. If you're encoding a piece of a URL, use encodeURIComponent(). If you're encoding a whole URL you already trust, use encodeURI(). Nine times out of ten you want the component version, because you're usually inserting a value into a query string.

Here's the failure mode. Say you're building a redirect: ?next= plus a target URL. Use encodeURI() on the target and its ? and & survive unescaped, so your outer URL now has two query strings fused together. Use encodeURIComponent() and the target becomes one opaque value that round-trips cleanly.

Modern code can skip the decision entirely. The URLSearchParams API encodes values for you, and it's part of the WHATWG URL Standard, the living spec that browsers actually implement. That spec states outright that one of its goals is to align RFC 3986 and RFC 3987 with contemporary implementations and obsolete those RFCs in the process. So the IETF documents define the format, and WHATWG defines what your browser really does with it.

Worth knowing which of the two is still moving. RFC 3986 has not changed since January 2005, which is the point of an Internet Standard: implementers need a fixed target. The WHATWG URL Standard is a Living Standard with no version numbers, and the copy we checked had been revised on 18 August 2026. It changes whenever browser behaviour changes.

That difference decides which document answers your question. If you're asking what a URL is allowed to look like, read the RFC, because it's stable and precise. If you're asking what Chrome will actually do with the string you just handed it, read the WHATWG spec, because it's the one tracking real implementations. Reaching for the RFC to predict browser behaviour is how you end up confidently wrong about an edge case.

Does the right encoding depend on where in the URL you are?

Yes, and this is the rule underneath the encodeURI versus encodeURIComponent choice above. RFC 3986 doesn't define one allowed character set for a whole URL. It defines a separate grammar for each component, and the sets genuinely differ.

Here's the actual ABNF from RFC 3986, which is shorter than most people expect:

ComponentGrammar
userinfo*( unreserved / pct-encoded / sub-delims / ":" )
path segment (pchar)unreserved / pct-encoded / sub-delims / ":" / "@"
query*( pchar / "/" / "?" )
fragment*( pchar / "/" / "?" )

Read down that table and three things fall out.

Query and fragment have identical rules. Both are pchar plus / and ?. So a literal slash in a query value is legal and needs no encoding. A question mark after the first one is also legal, which is why ?a=1?2 parses fine.

The path is stricter than the query. A path segment is pchar, which does not include / or ?. The slash is what separates segments, so a slash inside a filename has to become %2F. That distinction is the whole reason you can't run one encoder over a full path.

userinfo is the odd one out. It allows : but not @ and not /, because both terminate it. So a password containing @ breaks the URL unless you encode it.

What this means for encodeURIComponent

It over-encodes, and mostly that's fine. Run it on a query value and legal characters like / come back as %2F. The server decodes them to the same thing, so nothing breaks. You just get longer, uglier URLs than the spec requires.

Under-encoding is the direction that actually hurts. Leaving a raw & or = in a query value splits one parameter into two. Leaving a raw # anywhere truncates everything after it, because # starts the fragment and the fragment is never sent to the server at all.

So the safe habit is unchanged: encode per value, not per URL. The grammar above just explains why that works rather than asking you to memorise a function name.

The credentials trap

The userinfo row has a practical cost attached, and it shows up in database connection strings, git remotes and API URLs constantly. A password with @, : or / in it must be percent-encoded, or the parser reads your password as a hostname and hands you an error that points nowhere near the real problem.

But the spec would rather you didn't do this at all. Section 3.2.1 states plainly that "use of the format 'user:password' in the userinfo field is deprecated," notes that applications "should reject the storage of such data in unencrypted form," and observes that "passing of authentication information in clear text has proven to be a security risk."

There's a second warning in the same section that connects straight to the parser-confusion research further down this page. RFC 3986 tells user agents to "render userinfo in a way that is distinguished from the rest of a URI" precisely because the component can be "maliciously crafted to impersonate trusted domains." That's the classic https://trusted-site.com@attacker.example/ shape. Everything before the @ is a username. The host is whatever follows it.

Practical version: put credentials in a header or a config file, not in a URL. If you genuinely can't, encode every reserved character in the password and expect the string to end up in a log somewhere.

Once the per-component grammar clicks, the rest of this page stops feeling like a list of exceptions. Our URL encoder shows you the output for any string, which is the quickest way to check a value before it goes anywhere near production.

Why does a space sometimes become a plus sign?

Because there are two encoding contexts and they disagree about spaces.

In a URL, RFC 3986 says a space is %20. But HTML form submissions use an older format called application/x-www-form-urlencoded, and there a space becomes +. The WHATWG URL Standard keeps this behaviour alive with an explicit spaceAsPlus flag, and it is set true only when the percent-encode set is the application/x-www-form-urlencoded one. Nowhere else.

Rule of thumb: decode with the same convention the data was encoded with. If it came from a form post or a URLSearchParams serialisation, treat + as a space. If it came from a path or a hand-built URL, treat + as a literal plus sign.

This is a real source of corrupted data. A password containing + that gets decoded with form rules turns into a password containing a space, and the login fails for reasons nobody can reproduce.

Do you need to percent-encode Base64 in a URL?

Usually not, because there is a Base64 variant built for exactly this and most people never hear about it. If you are percent-encoding Base64 output to get it through a URL, you are solving a problem that already has a cleaner answer.

Here is why standard Base64 and URLs fight. The standard alphabet ends with + and /, and both of those mean something in a URL. Drop a token in a query string and the + comes out the other end as a space, for exactly the reason the plus-sign section above explains. Drop one in a path and the / reads as a path separator. And = padding on the end is its own small nuisance.

So people reach for percent-encoding, which does work. It also inflates the string, makes it unreadable in a log, and gives you one more layer to get wrong when something double-decodes.

The specification saw this coming. RFC 4648 Section 5 is titled "Base 64 Encoding with URL and Filename Safe Alphabet", and it swaps the two offending characters: + becomes - and / becomes _. Everything else is identical. As the RFC puts it, the variant "is technically identical to the previous one, except for the 62:nd and 63:rd alphabet character".

Both replacements are unreserved characters, so they travel through a URL untouched. No percent-encoding, no expansion, no second decode step.

Where you have already seen this: every JWT. RFC 7519 says "a JWT is represented as a sequence of URL-safe parts separated by period ('.') characters. Each part contains a base64url-encoded value." That is why a token drops into a URL without breaking, and why the parts contain dashes and underscores rather than plus and slash.

Padding gets handled too, and the rule is stricter than most people assume. RFC 7515 defines base64url encoding as the RFC 4648 Section 5 alphabet "with all trailing '=' characters omitted (as permitted by Section 3.2) and without the inclusion of any line breaks, whitespace, or other additional characters". So in the JOSE specs the padding is not optional to keep. It goes.

Which produces a decoding trap worth knowing. Plenty of standard Base64 decoders reject input whose length is not a multiple of four. Hand them a stripped base64url string and they fail on valid data. The fix is to pad it back out to a multiple of four with = before decoding, or use a decoder that does it for you.

What to actually do, depending on where you are.

  • Generating a token or identifier for a URL: reach for your language's base64url encoder rather than base64 plus percent-encoding. Python has urlsafe_b64encode, and most standard libraries have an equivalent.
  • Receiving something with dashes and underscores: that is base64url, not a corrupted string. Decode it as base64url, or translate - and _ back before a standard decode.
  • Debugging a token that decodes to nonsense: check the alphabet before you check anything else. A standard decoder fed base64url will produce plausible-looking garbage rather than an obvious error.
  • Still stuck with standard Base64: percent-encoding it is legitimate. Just do it once, and be sure nothing downstream decodes twice.

And the point the section on encryption already made applies here unchanged. Base64url is not security. It is an alphabet swap so a string survives a trip through a URL. Anyone can reverse it, and a JWT payload is readable by whoever holds the token.

Convert Base64 Now

Our Base64 encoder and decoder handles both alphabets, and the URL encoder is there when percent-encoding really is the right tool.

Does URL encoding work the same in other languages?

No, and the differences land on exactly the split the last section described. Most languages ship two encoders, one that follows RFC 3986 and one that does form encoding. They just don't agree on which one gets the obvious name.

Java is the sharpest case, because it doesn't ship an RFC 3986 encoder in the class you'd look in. Oracle's Javadoc for java.net.URLEncoder opens by calling it a "Utility class for HTML form encoding" and states that "The space character " " is converted into a plus sign "+"". So the class every Java developer reaches for is a form encoder wearing a URL name. Use it on a path segment and you get plus signs where you needed %20.

LanguageFunctionSpace becomesFollows
JavaScriptencodeURIComponent()%20RFC 3986
Pythonurllib.parse.quote()%20RFC 3986, but safe='/' by default
Pythonurllib.parse.quote_plus()+Form encoding
PHPrawurlencode()%20RFC 3986
PHPurlencode()+Form encoding
JavaURLEncoder.encode()+Form encoding only

Python's default won't encode a slash. The Python documentation says the safe parameter "specifies additional ASCII characters that should not be quoted" and that "its default value is '/'". That's sensible when you're quoting a whole path and wrong when you're quoting one segment that happens to contain a slash. Pass safe='' to get encodeURIComponent() behaviour. It's the same trap as the JavaScript pair, just with a keyword argument instead of a different function name.

PHP's two functions disagree about the tilde. The manual says rawurlencode() replaces "all non-alphanumeric characters except -_.~", while urlencode() replaces all of them "except -_.". That missing tilde matters, because RFC 3986 lists ~ as unreserved and says unreserved characters should never be percent-encoded. So urlencode() emits %7E where the standard says leave it alone. PHP's own manual is blunt about why, noting the difference exists "for historical reasons".

Java manages to miss in both directions at once. Its Javadoc says the special characters ., -, * and _ remain the same. The tilde isn't on that list so it gets encoded, and the asterisk is on it so it doesn't, even though RFC 3986 classes * as a sub-delimiter. Over-encode one, under-encode the other.

Quick test for any language: encode the string a~b c*d and read the answer.
a~b%20c%2Ad is Python's quote() or PHP's rawurlencode().
a~b%20c*d is JavaScript's encodeURIComponent(), which leaves the asterisk alone.
a%7Eb+c*d is Java's URLEncoder.
a%7Eb+c%2Ad is PHP's urlencode().
Four functions, four different answers, same input. The space tells you whether you're holding a form encoder. The tilde and asterisk tell you how closely it tracks RFC 3986.

None of this shows up while your test data is alphanumeric, which is precisely why it survives code review and breaks in production. If you're passing encoded values between services written in different languages, encode and decode at the same boundary with the same convention, and write down which one you picked.

How do you encode a URL for a signed API request?

Exactly the way that API's spec tells you to, character for character, because the signature is computed over the encoded string. Change one byte and the server computes a different signature and hands you a 403. There's no partial credit.

This is the one place where close enough stops being cosmetic. Every other section on this page is about a URL that works or doesn't. Signed requests are about a URL that authenticates or doesn't, and the rejection almost never tells you which character went wrong.

OAuth 1.0a is the strict one. RFC 5849, section 3.6 writes out its own percent-encoding rules rather than pointing at RFC 3986 and hoping. Unreserved characters "MUST NOT be encoded", all other characters "MUST be encoded", and then the line that catches people: "The two hexadecimal characters used to represent encoded characters MUST be uppercase."

Uppercase. So %2f fails where %2F passes, even though the equivalence section below explains that RFC 3986 treats those two as the same thing. A signature doesn't care about equivalence. It hashes bytes. RFC 5849 also flags that its encoding differs from application/x-www-form-urlencoded, which uses a plus sign for a space instead of %20, so running a signature base string through a form encoder gets every space wrong at once.

AWS Signature Version 4 goes further and tells you not to trust your language. The AWS documentation for creating a signed request defines its own UriEncode() with five rules:

  • Encode every byte except the unreserved set: A-Z, a-z, 0-9, hyphen, period, underscore and tilde.
  • Treat space as reserved and encode it as %20, never as +.
  • Build each escape from a percent sign and the two-digit hex value of the byte.
  • Make the hex letters uppercase, as in %1A.
  • Encode the forward slash everywhere except inside an S3 object key name.

That last rule is the odd one out. If the object key is photos/Jan/sample.jpg, the slashes inside the key stay as slashes so the canonical URI keeps its shape, while every other slash in the request gets escaped.

Then AWS says the quiet part out loud. Its documentation warns that "The standard UriEncode functions provided by your development platform might not work because of differences in implementation and related ambiguity in the underlying RFCs," and recommends you "write your own custom UriEncode function to make sure that your encoding will work."

Read that next to the four-function test above and it stops sounding paranoid. Amazon isn't warning you about badly written libraries. It's warning you about the exact tilde-and-asterisk spread you can measure yourself in four lines of code, because a signature has no tolerance for it.

Why signature bugs are miserable to debug: a mis-encoded byte produces a perfectly valid-looking request with a wrong signature. The server can't tell you which character disagreed, because it never saw your string. It only ever saw a hash.

Three habits that save you the afternoon.

  • Use the official SDK unless you have a real reason not to. AWS opens its own manual-signing page by telling you to skip the whole section if you're using an SDK or the CLI, and it means it.
  • Log the canonical string, not just the signature. Most signing schemes define an intermediate string that gets hashed. Print yours, print the one a known-good client produces, and run them through a text diff. The mismatch is nearly always a single character.
  • Uppercase your hex on the way out. If you are hand-rolling an encoder, normalise the case of every escape as the last step. One line, and an entire category of 403 disappears.

What happens to non-English characters in a URL?

Here's where a lot of otherwise solid mental models quietly fall apart. Percent-encoding handles non-ASCII text in the path, the query and the fragment. It does not handle the hostname. The domain runs on an entirely different scheme, and confusing the two produces links that look correct and resolve nowhere.

In the path, the rule is the one you already know. The word café becomes caf%C3%A9, because the accented e is two bytes in UTF-8. A rocket emoji becomes %F0%9F%9A%80, four bytes and therefore four escapes.

In the host, none of that applies. Domains are converted with Punycode, an algorithm Adam Costello specified for the IETF in RFC 3492 in March 2003. RFC 5891, the IDNA2008 protocol document, spells out the conversion: "The A-label is the encoding of the U-label according to the Punycode algorithm with the ACE prefix 'xn--' added at the beginning of the string." Every internationalised domain you have ever seen carries that four-character prefix once it hits the wire.

What you typeWhat actually resolves
münchen.dexn--mnchen-3ya.de
café.frxn--caf-dma.fr
日本.jpxn--wgv71a.jp

RFC 5891 also caps each converted label at 63 characters in its ACE form, which is tighter than it sounds once a handful of non-ASCII characters have expanded. And the WHATWG URL Standard defines 17 forbidden host code points, including NULL, tab, newline, space, #, /, :, ?, @ and the square brackets. You cannot percent-encode your way around them either, because the parser decodes first: it builds the domain from "the result of running UTF-8 decode without BOM on the percent-decoding of input," then rejects anything forbidden that turns up. Sending %2F in a hostname just gets you a slash and a parse failure.

The security consequence is the part worth internalising. Because two visually identical characters can sit at different code points, a Punycode domain can render as something it isn't. Suzuki, Chiba, Yoneya, Mori and Goto measured this at the ACM Internet Measurement Conference in 2019 with a framework they called ShamFinder. Scanning 141.2 million unique .com domains collected in May 2019, they found 955,512 internationalised domain names, or 0.67% of the total, and 1,647 IDN homographs that were still reachable over HTTP or HTTPS, roughly half of everything they detected. Their headline case was gmaıl.com, spelled with a Turkish dotless i, which was an active phishing site with 615,447 recorded name resolutions. It travels as xn--gmal-nza.com.

Practical version: if you're logging, comparing, or allowlisting domains, normalise to the xn-- form first. Two hosts that look the same on screen are not the same host, and the Punycode string is the only representation that tells you the truth.

How long can a URL be after encoding?

There's no limit in the spec, but there is a practical floor you should design against: 8000 characters. Encoding is usually what pushes a URL past it, because every byte you encode gets three times longer.

The HTTP standard is deliberately quiet here. RFC 9112, section 3 says plainly that "HTTP does not place a predefined limit on the length of a request-line," then adds the number everyone actually builds to: "It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets." So 8000 isn't a maximum. It's the smallest ceiling you're allowed to assume other people have.

Go past what a server is willing to parse and the same RFC says it "MUST respond with a 414 (URI Too Long) status code." MDN's reference for 414 lists the usual culprits, and the first one is the giveaway: a client that "improperly converted a POST request to a GET request with long query information."

Here's why encoding matters so much. A percent-encoded octet is always three characters, so the cost depends entirely on how many bytes your character takes in UTF-8.

CharacterUTF-8 bytesEncodedLength
space1%203x
&1%263x
e with acute accent2%C3%A96 chars
a CJK character3%E6%96%879 chars
an emoji4%F0%9F%98%8012 chars

That last row is the one that catches people. A 200-character search phrase in English stays roughly 200 characters if it's mostly letters and digits, because unreserved characters pass through untouched. The same 200 characters of Japanese or Chinese becomes about 1800. Add a few emoji and you're over 2000 before you've attached a single tracking parameter. The URL looked short in your editor. It isn't short on the wire.

And the length that counts is the encoded length, measured after every layer has done its work. If you're near the line, double encoding will put you over it, because each pass multiplies again.

Rule of thumb: if a value is user-generated, unbounded, or non-Latin, don't put it in the query string. Send it in a POST body. Query parameters are for short, known-shape values like ids, flags, and pagination.

What happens when your URL travels through plain text?

It stops being a URL. It becomes a run of characters that somebody else's software has to guess the boundaries of, and encoding choices that are perfectly correct inside your app start producing links that arrive broken.

This isn't a new problem, and the spec called it twenty years ago. RFC 3986 closes with Appendix C, "Delimiting a URI in Context", which opens by naming the situation exactly: "URIs are often transmitted through formats that do not provide a clear context for their interpretation. For example, there are many occasions when a URI is included in plain text; examples include text sent in email, USENET news, and on printed paper."

Then it names the actual failure. In those cases "it is important to be able to delimit the URI from the rest of the text, and in particular from punctuation marks that might be mistaken for part of the URI."

That's the full stop at the end of your sentence. It's the closing bracket when you put a link in parentheses. Every autolinker on earth has to decide whether those belong to your URL or to the prose around it, and they don't all decide the same way.

What the spec tells you to do about it

Wrap it. The appendix notes that URIs get delimited "within double-quotes, angle brackets, or just by using whitespace", then makes a recommendation: "Using <> angle brackets around each URI is especially recommended as a delimiting style for a reference that contains embedded whitespace." And it settles the obvious follow-up question in five words: "These wrappers do not form part of the URI."

So a link in an email is safer as <https://example.com/a b> than bare. Though better still is not shipping a raw space in the first place, which is the whole argument for encoding it to %20 before the URL ever leaves your system.

There's a wrapper the appendix explicitly retires, too. The prefix "URL:" was "formerly recommended as a way to help distinguish a URI from other bracketed designators, though it is not commonly used in practice and is no longer recommended". If you've seen that in old documentation, it's dead.

The hyphen trap almost nobody knows about

This is the strangest thing in the appendix and it's genuinely useful. Long URLs get wrapped across lines, and the spec accepts that: "In some cases, extra whitespace (spaces, line-breaks, tabs, etc.) may have to be added to break a long URI across lines. The whitespace should be ignored when the URI is extracted."

But one break point is poisoned. The spec says "No whitespace should be introduced after a hyphen ('-') character. Because some typesetters and printers may (erroneously) introduce a hyphen at the end of line when breaking it, the interpreter of a URI containing a line break immediately after a hyphen should ignore all whitespace around the line break and should be aware that the hyphen may or may not actually be part of the URI."

Read that last clause again. If your URL wraps right after a hyphen, nobody downstream can tell whether the hyphen is yours or the typesetter's. It's ambiguous by construction. Which is a real argument against hyphen-heavy slugs on anything destined for print, and it pairs with the length section above: the URLs that get wrapped are the long ones, and encoding is what makes them long.

What browsers actually do now

They implement the appendix's advice, and the living standard writes it down as steps. The WHATWG basic URL parser begins by handling exactly this mess. It will "Remove any leading and trailing C0 control or space from input." Then, if the input "contains any ASCII tab or newline", it raises an invalid-URL-unit validation error, and immediately afterwards it will "Remove all ASCII tab or newline from input."

Notice it does both. Your pasted URL with a line break in the middle is formally invalid and the parser says so, then cleans it up and carries on anyway. That's why a link mangled by an email client so often still works when you paste it into the address bar, and why you can't rely on that behaviour for anything that isn't a browser.

Practical rules for links that leave your app

  • Encode spaces before you send, not after. A %20 survives a copy-paste, a line wrap and a spam filter. A raw space survives none of them reliably.
  • Wrap in angle brackets in plain text. Especially in email and anywhere you don't control how the text is rendered.
  • Keep trailing punctuation off the link. If a URL ends a sentence, the bracket wrapper removes the ambiguity entirely.
  • Stick to unreserved characters for anything printed or typed. Letters, digits, hyphen, period, underscore and tilde travel through any medium unchanged. Our slug generator guide covers building URLs that way from the start.

If you want to see what a string looks like once it's safe to send, run it through the URL encoder and copy the result rather than the original.

Are two differently encoded URLs the same?

Sometimes, and the spec tells you exactly when. Two URLs that differ only in the case of their hex digits, or in whether an unreserved character was encoded, point to the same resource. Two that differ in an encoded reserved character do not.

Start with the easy one. RFC 3986, section 6.2.2.1 states that "the hexadecimal digits within a percent-encoding triplet (e.g., '%3a' versus '%3A') are case-insensitive and therefore should be normalized to use uppercase letters for the digits A-F." So %3a and %3A are the same character. Uppercase is the canonical form, and it's what every well-behaved encoder emits.

The second rule is about characters that never needed encoding. Section 2.3 of the same document says percent-encoded octets for letters, digits, hyphen, period, underscore, or tilde "should not be created by URI producers and, when found in a URI, should be decoded to their corresponding unreserved characters by URI normalizers." In plain terms, %7E and ~ are the same URL, and the tilde is the correct way to write it. If your encoder is producing %41 for a capital A, it's being needlessly aggressive.

But equivalence stops at reserved characters. An encoded slash is not a slash, because decoding it would change how the path splits. That's a structural difference, not a cosmetic one, and no normalizer is allowed to collapse it.

PairSame resource?Why
%3a and %3AYesHex digits are case-insensitive
%7E and ~YesTilde is unreserved
%2F and /NoSlash is reserved and structural
%20 and +DependsOnly equivalent in form-encoded data

Why should you care? Because anything that keys off a URL string will treat these as different: caches, rate limiters, analytics rollups, deduplication jobs, and search engines deciding which version of a page to index. Encode inconsistently across your codebase and you'll split one page into several in your own reporting, with no error anywhere to tell you it happened.

Rule of thumb: normalise before you compare or store. Uppercase the hex digits, decode any encoded unreserved characters, and leave every reserved character exactly as you found it.

Does URL encoding affect your SEO?

Yes, but not the way people hope. Encoding a URL correctly won't win you rankings. Encoding it inconsistently will quietly split one page into several as far as a crawler is concerned, and that's the part that costs you.

Google is unusually specific about which standard it follows. Its URL structure documentation says Search supports URLs as defined by IETF STD 66, which is RFC 3986 under its standard number, and that "Characters defined by the standard as reserved must be percent encoded." Unreserved ASCII characters "may be left in the non-encoded form", and non-ASCII characters should be UTF-8 encoded and then percent-encoded.

None of that contradicts anything above. What's specific to search is what happens when you're inconsistent about it.

Go back to the equivalence rules. %7E and ~ are the same resource. %3a and %3A are the same resource. A crawler still has to do that normalisation before it knows, and a crawler that reaches one page through three different spellings has three URLs to fetch, compare and reconcile for one piece of content. That's crawl effort spent on nothing, and in the worse cases it's a canonical decision you didn't get to make.

Google's guidance on non-ASCII text is blunter than most documentation gets. It lists https://example.com/%D9%86%D8%B9%D9%86%D8%A7%D8%B9/%D8%A8%D9%82%D8%A7%D9%84%D8%A9 as recommended, and the same path written with the Arabic characters left raw as not recommended. The encoded version is the ugly one to read and the one Google asks for.

The same page also tells you to "Use words in your audience's language in the URL (and, if applicable, transliterated words)", with German and Japanese examples alongside the Arabic. Those two instructions work together: write the slug in your reader's language, then percent-encode it before it ships. If you're generating those slugs from titles, a slug generator handles the awkward characters before they ever reach a URL.

Four things to keep aligned, and honestly that's the whole job:

  • Internal links, canonical tags and your sitemap need the identical spelling. Not an equivalent one. The identical one. A canonical that says %7E while every link on the site says ~ is a contradiction you're asking a crawler to resolve for you.
  • Pick one case for hex digits. RFC 3986 prefers uppercase, so use uppercase, everywhere, including in your sitemap.
  • Don't encode what doesn't need encoding. Percent-encoding an unreserved character is legal and pointless, and it manufactures a second spelling of a URL that was already fine.
  • Watch your tracking parameters. Campaign tags are the usual source of near-duplicate URLs, and an inconsistently encoded value makes a mess of the reporting too. Our UTM parameters guide covers keeping those clean.

Worth remembering: correct encoding buys you the absence of a problem, not a ranking boost. That's most of what technical SEO actually is.

Why does a correctly encoded %2F still 404?

Because the server in front of you gets a vote, and by default Apache votes no.

This is the part that catches people who did everything right. A slash inside a path segment has to be encoded, exactly as covered above. So you encode it, you ship it, and production returns a 404 for a file you can see sitting on disk.

Apache's AllowEncodedSlashes directive is why. It has three settings and the default is the strictest one.

SettingWhat happens to a request containing %2F
Off (default)Refused with a 404, and the same applies to %5C on systems that treat backslash as a separator
OnAccepted, and the encoded slash is decoded like any other encoded character
NoDecodeAccepted, but the encoded slash is left in its encoded state

Note which one Apache itself recommends. The documentation says that if you need encoded slashes in path info, NoDecode is strongly recommended as a security measure, because letting slashes be decoded could allow unsafe paths.

That warning connects straight to the path traversal problem further down this page. Turning the directive to On doesn't just make your filenames work. It hands the decoder a slash it didn't have before, which is precisely the primitive an attacker wants.

So the practical rules are dull but they'll save you an afternoon.

  • Don't put slashes in path segments if you can avoid it. Move the value into the query string, where a %2F is nobody's business but your application's. This is almost always the right call.
  • If you genuinely need it in the path, use NoDecode and have your application decode the segment itself, so the decoding happens somewhere you control.
  • Test on the real stack. Your local dev server may not be Apache, and a URL that works on your machine can 404 the moment it goes behind a different one.

What happens if the same parameter appears twice?

Nobody agrees, and that's not an exaggeration. Five common stacks give five different answers.

Send ?id=1&id=2 and ask for a single value. You might get the first one, the last one, or both stuck together. The specs don't settle it, so the answer depends on the language and server you happen to be running.

Marco Balduzzi, Carmen Torrano Gimenez, Davide Balzarotti and Engin Kirda measured exactly this in "Automated Discovery of Parameter Pollution Vulnerabilities in Web Applications", presented at NDSS 2011. Their table of tested behaviour is worth memorising if you work across stacks.

Technology and serverMethod testedWhich value you get
ASP / IISRequest.QueryString("par")All of them, joined into one comma-delimited string
PHP / Apache$_GET["par"]Last
JSP / TomcatRequest.getParameter("par")First
Perl CGI / ApacheParam("par")First
Python / Apachegetvalue("par")All of them, as a list

Getting one value back isn't a bug on its own. The trouble starts when an attacker can inject an encoded delimiter into a parameter you echo into a link, because they can then append a parameter of their own and count on your stack to prefer theirs over yours.

The scale surprised people at the time. The researchers built a scanner called PAPAS and pointed it at more than 5,000 popular websites drawn from Alexa's category rankings. It found that 1,499 of them, or 29.88 percent, had at least one page vulnerable to HTTP Parameter Injection. Of those 1,499, at least 702 (46.8 percent, which works out to 14 percent of every site tested) could be exploited to override a hard-coded parameter or inject a new one. The affected list included Google, PayPal, Symantec, Microsoft and VMware, which tells you this isn't a beginner's mistake.

Two habits follow from that.

  • Ask for parameters explicitly as lists when your language offers it, so duplicates become visible data rather than a silent coin flip. Reject or handle the multi-value case on purpose.
  • Encode values before you build a URL from them, every time, with no exceptions for values you believe are safe. An unencoded & inside a value is the whole attack.

What mistakes break URLs most often?

Four patterns account for most of the damage.

Double encoding. You encode a value, then some middleware encodes it again. The % in %20 becomes %25, so %20 turns into %2520 and your space arrives as the literal text %20. Encode exactly once, at the point you build the URL.

Double encoding isn't only a data bug. It's an attack technique, and it's worth knowing why. OWASP describes it as "encoding user request parameters twice in hexadecimal format in order to bypass security controls," and the mechanism is simple enough to be uncomfortable: "by using double encoding it's possible to bypass security filters that only decode user input once."

Path traversal is the textbook case. Block ../ and an attacker sends %2E%2E%2F. Block that too and they send %252E%252E%252F, which your filter decodes once into %2E%2E%2F, sees as harmless, and passes to a backend that decodes it again into ../. This is not theoretical. OWASP cites the IIS vulnerability CVE-2001-0333 as a real example.

The practical takeaway, which is ours rather than OWASP's, is about ordering. Decode fully first, then validate the result. Validating a string that still holds encoded characters means you're checking something the system will later turn into something else.

Not encoding at all runs a close second, usually because the test data was all lowercase letters with no punctuation. Then a customer named O'Brien signs up and the apostrophe takes down the page. If you're generating URL-safe strings from titles, a slug generator sidesteps the problem by stripping the risky characters before they ever reach a URL.

Encoding the whole URL instead of the parts. Running encodeURIComponent() over an entire URL escapes the :// and every slash, giving you a string that's no longer a URL at all.

Assuming encoding is security. Percent-encoding is not sanitisation. It stops a value from breaking URL structure. It does not stop SQL injection, and attackers routinely use encoding variations to slip past naive filters. Validate on the server after decoding, every time.

Can bad encoding turn into a security hole?

Yes, and the mechanism is more interesting than "someone typed a quote mark". The problem is that two libraries in your own stack can read the same URL and disagree about what it means.

Claroty's Team82 and Snyk published joint research on this on 10 January 2022, testing 16 URL parsing libraries across languages: urllib, urllib3, rfc3986 and httptools in Python, curl lib, Wget, Chrome, .NET's Uri, Java's URL and URI, PHP's parse_url, url and url-parse in Node, Go's net/url, Ruby's uri, and Perl's URI. Their write-up is Exploiting URL Parsing Confusion.

They sorted the disagreements into five categories: scheme confusion, slash confusion, backslash confusion, scheme mixup, and the one that belongs on this page, URL-encoded data confusion. That last category is exactly what it sounds like. Feed two parsers a URL containing percent-encoded characters and they can come back with different hosts, different paths, or different query values.

This isn't theoretical. The research produced eight CVEs in real, widely used software:

  • Flask-security, Flask-security-too, Flask-User and Flask-unchained in Python (CVE-2021-23385, CVE-2021-32618, CVE-2021-23401, CVE-2021-23393)
  • Belledonne's SIP Stack in C (CVE-2021-33056)
  • Video.js in JavaScript (CVE-2021-23414)
  • Nagios XI in PHP (CVE-2021-37352)
  • Clearance in Ruby (CVE-2021-23435)

A bug in libcurl came out of the same work, disclosed to curl's creator Daniel Stenberg and patched. Claroty counts it inside the eight; Snyk's write-up lists the eight CVE identifiers above without it. Either way it's one body of research, not nine separate holes.

The pattern behind most of those is the same, and it's worth holding on to. An application validates a URL with one parser, decides it's safe, then hands the original string to a different component that parses it differently and fetches somewhere else entirely. That gap is how open redirects and server-side request forgery happen. Your validator said one thing, your HTTP client did another.

Four habits that close most of it:

  • Parse once, then pass the parsed object. Don't validate a string and then hand the raw string onward. Validate the components you extracted and use those.
  • Use the same parser for checking and for fetching. Mixing a validation library with a different HTTP client's internal parser is the exact setup that produced those CVEs.
  • Decode fully before you validate. Checking a raw string for a blocked host misses the version where the host arrives percent-encoded, which loops back to the double-encoding problem above.
  • Never hand-roll URL parsing with a regex. The spec has too many edge cases, and the sixteen libraries above are maintained by people who have already lost this fight once.

None of this means encoding is dangerous. It means encoding is a place where implementations differ, and anywhere implementations differ is somewhere an attacker will look.

What do developers ask about URL encoding?

What is URL encoding used for?

URL encoding lets you put arbitrary data inside a URL without breaking its structure. It is what makes search queries, form submissions, API parameters, redirect targets, and non-English text work in a link. Without it, a single ampersand or question mark in user input would split the URL into the wrong pieces and the request would fail or go somewhere unintended.

Which characters need to be URL encoded?

RFC 3986 defines 66 unreserved characters that never need encoding: A to Z, a to z, 0 to 9, plus hyphen, period, underscore and tilde. Everything else should be encoded when it appears as data rather than structure. That includes the 18 reserved characters, spaces, and every non-ASCII character such as accented letters or emoji.

What is the difference between encodeURI and encodeURIComponent?

encodeURI leaves 82 characters untouched because it assumes you are handing it a whole URL that still needs its structure. encodeURIComponent leaves only 71 untouched and encodes the 11 reserved characters that encodeURI preserves. Use encodeURIComponent for individual query values and path segments. Use encodeURI only for a complete URL you already trust.

Why is a space encoded as %20 sometimes and + other times?

Both are correct in their own context. %20 is the percent-encoding of a space defined by RFC 3986 and works anywhere in a URL. The plus sign comes from the older application/x-www-form-urlencoded format used by HTML form submissions, where the WHATWG URL Standard converts spaces to plus. Decode with the same rules the data was encoded with.

Is URL encoding the same as encryption?

No. URL encoding is a public, reversible transformation with no key involved. Anyone can decode a percent-encoded string instantly, and browsers do it automatically. It hides nothing. Encoding exists to keep data intact in transit, not to keep it secret. If you need confidentiality, use HTTPS and real encryption instead.

Try It Yourself

Paste any string into our URL encoder and decoder to see the percent-escapes, or run a query string back the other way to find out what a link is really carrying.

Open URL Encoder →

Sources: IETF RFC 3986, Uniform Resource Identifier (URI): Generic Syntax, for the per-component ABNF grammar covering userinfo, pchar, query and fragment, and for Section 3.2.1 deprecating the user:password format in userinfo, advising that applications should reject storage of such data in unencrypted form, noting that passing authentication information in clear text has proven to be a security risk, and directing user agents to render userinfo distinguishably because it can be maliciously crafted to impersonate trusted domains. IETF, RFC 3986 "Uniform Resource Identifier (URI): Generic Syntax", January 2005, STD 66 (ietf.org). MDN Web Docs, "Percent-encoding" and "encodeURIComponent()" (developer.mozilla.org). WHATWG, "URL Standard" (url.spec.whatwg.org), for the goal of obsoleting RFC 3986 and RFC 3987, the forbidden host code points, the space-as-plus flag, and the 18 August 2026 revision date cited above, which will have moved on by the time you read it. A. Costello, IETF, RFC 3492 "Punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", March 2003 (rfc-editor.org). IETF, RFC 5891 "Internationalized Domain Names in Applications (IDNA): Protocol", August 2010 (rfc-editor.org). H. Suzuki, D. Chiba, Y. Yoneya, T. Mori and S. Goto, "ShamFinder: An Automated Framework for Detecting IDN Homographs", Proceedings of the ACM Internet Measurement Conference (IMC '19), 2019. OWASP, "Double Encoding" (owasp.org). Python Software Foundation, "urllib.parse" documentation (docs.python.org), for the safe parameter default and the quote_plus behaviour. The PHP Group, "urlencode" and "rawurlencode" manual pages (php.net), for the two unreserved sets and the "historical reasons" note on spaces. Oracle, java.net.URLEncoder Javadoc, Java SE 21 (docs.oracle.com), for the HTML form encoding description, the plus sign conversion, and the list of characters left unchanged. Claroty Team82 and Snyk, "Exploiting URL Parsing Confusion", published 10 January 2022 (claroty.com), for the 16 libraries tested and the five categories of inconsistency including URL-encoded data confusion. The eight CVE identifiers are as listed by Snyk in "URL confusion vulnerabilities in the wild: Exploring parser inconsistencies" (snyk.io); note that Claroty describes the libcurl bug as one of the eight while Snyk's enumeration of the eight CVEs does not include it, so we have reported both rather than picking one. IETF, RFC 3986, Section 6.2.2 (rfc-editor.org), for the case normalisation rule on hexadecimal digits, the Section 6.2.2.2 requirement that percent-encoded octets corresponding to unreserved characters be decoded by URI normalizers, and the quoted list of contexts in which URI comparison occurs.