Web Development

Special Characters in URL Paths vs Query Strings: The Complete Guide

Master URL encoding for both paths and query strings, understanding the differences, security implications, and best practices for each.

By Inventive HQ Team

URL paths and query strings both use percent-encoding, but they follow different rules about which characters are safe to leave unencoded. In a path segment you may use the unreserved characters (letters, digits, - . _ ~), the sub-delimiters, and additionally : and @; a query string also allows / and ? unencoded. The characters that carry structural meaning — the / between path segments and the & and = between query parameters — must be percent-encoded (%2F, %26, %3D) when they appear inside a value. A space is %20 everywhere, but in a form-encoded query string it can also be written as +. In JavaScript, encode each individual value with encodeURIComponent() and reserve encodeURI() for a whole, already-assembled URL.

That paragraph is the summary an AI overview gives you. The rest of this article is what it can't: the exact per-component character table you can act on, why the +-means-space rule bites people, the one JavaScript function to reach for and the one bug that keeps encoding right, and how a missing %2F turns into a path-traversal vulnerability.

The anatomy of a URL (and why the boundaries matter)

Every rule below comes from one fact: a URL is not one string, it is a sequence of components with hard boundaries, and the same character can be a delimiter in one spot and data in another. A ? starts the query; a & separates parameters; a / separates path segments; a # starts the fragment. When you put user data into one of those components, your job is to make sure the data can't be mistaken for a delimiter.

The components of a URL and their delimiters A URL split into scheme, host, path, query, and fragment, with a marker sweeping across to highlight that each component has its own encoding rules. One URL, five components, different rules each https:// host.com /path/seg ? q=value&n=2 # frag scheme host path query fragment / separates segments · encode a data slash as %2F & and = separate params · encode them as %26 / %3D in values A character is a delimiter or data depending on the component it lands in.

When you examine a URL you see the pattern scheme://host:port/path?query#fragment. The path comes before the ? and identifies the resource. The query comes after and carries parameters. They look like one string but are distinct components with distinct encoding requirements — which is exactly where bugs and vulnerabilities creep in.

Path vs query string: the comparison table

This is the table to bookmark. It contrasts how each component treats the characters that trip people up, per RFC 3986.

ConcernURL Path (a segment)Query String (a value)
Always-safe unencodedUnreserved: A–Z a–z 0–9 - . _ ~Unreserved: A–Z a–z 0–9 - . _ ~
Also allowed unencodedsub-delims ! $ & ' ( ) * + , ; = plus : and @everything a path allows, plus / and ?
Space%20 (only valid encoding)%20, or + in application/x-www-form-urlencoded
Forward slash /Delimiter between segments; encode a data slash as %2FData only — safe unencoded, or %2F
& and =Ordinary data in a path (rare); usually left as-isDelimiters — encode inside a value as %26 / %3D
?Ends the path — encode a data ? as %3FData only after the first ? — safe, or %3F
#Starts the fragment — always encode as %23Starts the fragment — always encode as %23
+Literal plus signLiteral plus in raw URLs; means space in form-encoded data
[ ]Not allowed in a segment — encode as %5B %5DNot allowed — encode as %5B %5D
Right JS functionencodeURIComponent(segment)encodeURIComponent(name) and encodeURIComponent(value)

The single most important row is the last one: you don't hand-apply this table. You call encodeURIComponent() on each individual value, and it encodes every reserved delimiter for you. The table explains why the output looks the way it does — and why encodeURI() (which leaves / ? & = # alone) is the wrong tool for a value.

Loading interactive tool...
Advertisement

The +-means-space trap

The plus sign causes more confusion than any other character because its meaning is contextual. In the original URL specification, + is a literal plus sign everywhere. But the application/x-www-form-urlencoded serialization — the format an HTML <form> submits, and the format nearly every server framework assumes when it parses a query string — defines + as an encoded space.

The consequences are concrete:

  • A value of C++ placed in a form-encoded query becomes C%2B%2B. If you leave the pluses raw as C++, the server decodes them to C (two spaces).
  • A %20 in a query string is always safe: both the raw-URL rules and the form-encoded rules decode it to a space. That's why encodeURIComponent() emits %20 and never + — it's the one encoding that can't be misread.
  • In the path, a + is never a space. /a+b is a segment containing a literal plus. Servers that turn + into a space in the path are technically wrong, though some legacy stacks do it.

Rule of thumb: encode spaces as %20 and let the receiver's form parser handle + on the way in. Never emit + for a space yourself unless you're deliberately building a form-encoded body.

encodeURIComponent vs encodeURI: pick the right layer

JavaScript ships two encoders and getting them backwards is the number-one URL bug.

  • encodeURIComponent(str) — for a single component: one path segment, one parameter name, one parameter value. It encodes the reserved delimiters / ? : @ & = + $ , #, so the value can't break out of its slot. This is your default.
  • encodeURI(str) — for a whole, already-structured URL. It deliberately leaves / ? & = # : @ untouched so it doesn't corrupt the URL's own structure. Use it only to make an otherwise-complete URL valid (escaping spaces, non-ASCII), never on a value.
// RIGHT: encode each value, then assemble
const url = `/api/users/${encodeURIComponent(userId)}` +
            `?q=${encodeURIComponent(searchTerm)}`;

// WRONG: encodeURIComponent on the whole URL mangles the :// and the ? & =
encodeURIComponent("https://x.com/a?q=1&n=2");
// -> "https%3A%2F%2Fx.com%2Fa%3Fq%3D1%26n%3D2"  (now unusable as a URL)

// WRONG: encodeURI on a value fails to escape & and =, so the value
// "a&b=c" injects an extra parameter
`?q=${encodeURI("a&b=c")}`;   // -> "?q=a&b=c"  (parameter pollution)

Equivalents in other languages: Python's urllib.parse.quote() (path-safe, keeps /) vs quote_plus() (query-safe, encodes /, emits + for space); PHP's rawurlencode() (RFC 3986, %20) vs urlencode() (form-style, +); Java's URLEncoder.encode() (form-style, +) — reach for the component-level, %20-emitting variant unless you specifically want form encoding.

Why this is a security problem, not just a formatting one

Missing encoding lets an attacker change what a URL means. Three classic failures:

  • Path traversal. If a user-supplied identifier is dropped into a path unencoded, a value like ../../etc/passwd walks up the directory tree. Encoding the slashes (..%2F..%2Fetc%2Fpasswd) keeps the value in one segment — but the real fix is server-side validation that the identifier contains only allowed characters.
  • HTTP parameter pollution. An unencoded & or = inside a query value injects extra parameters: ?user=alice&role=admin smuggled in as a single user value. encodeURIComponent() turns the injected &/= into %26/%3D, confining them to the value.
  • Injection smuggling. Encoded payloads (%27 for ', %3C for <) are a standard way to slip SQL-injection or XSS characters past naive filters that only inspect the raw text. Correct encoding is defense-in-depth, never the whole defense: always decode, validate against an allow-list, and use parameterised queries and context-aware output escaping regardless of what the URL looked like.

Encoding confines a value to its field. Validation decides whether the value is allowed at all. You need both.

Common mistakes and how to avoid them

Double-encoding. Encoding an already-encoded value turns %20 into %2520 (the % itself becomes %25). The server decodes once and gets literal %20 back instead of a space. Encode exactly once, at the single point where the raw value enters the URL, and never re-encode a string that already contains percent-escapes.

Mixing encoders across the codebase. If some code uses encodeURIComponent() and other code uses encodeURI() (or a home-grown function) on the same values, you get inconsistent output and hard-to-trace bugs. Standardise on one approach — component-level encoding at the assembly point.

Assuming "trusted" data is safe. Internal identifiers, database keys, and generated filenames can all contain /, &, spaces, or Unicode. Encode every value that goes into a URL, regardless of source.

Hand-rolling encoding. Don't write your own percent-encoder. The standard library functions already handle the UTF-8-then-percent step for non-ASCII characters (%E2%82%AC) and the exact reserved sets. Reinventing them is how edge cases become vulnerabilities.

The bottom line

Paths and query strings share one encoding scheme (percent-encoding) but not one rulebook: a path segment may keep : and @ and treats / as structure; a query value must encode & and = but may keep / and ?; a space is %20 everywhere, and + only means space inside form-encoded query data. You almost never apply those rules by hand — you call encodeURIComponent() on each individual value, reserve encodeURI() for a finished URL, encode exactly once, and validate on the server no matter how clean the URL looks. Get those four habits right and the entire table above takes care of itself.

Frequently Asked Questions

What is the difference between encoding a URL path and a query string?

Both use percent-encoding (RFC 3986), but the set of characters that are "safe" to leave unencoded differs by component. A path segment may contain the unreserved characters, the sub-delimiters (! $ & ' ( ) * + , ; =), and additionally the colon and at-sign, so it never needs to encode a colon or an at-sign. A query string additionally allows the forward slash and question mark unencoded. The characters that carry structural meaning — the slash separating path segments, and the & and = separating query parameters — must be percent-encoded when they appear inside a value rather than as a delimiter. In practice you don't memorise these tables; you call encodeURIComponent() on every individual value and let it encode everything that isn't unreserved.

Does a plus sign (+) mean a space in a URL?

Only in one specific place: the query string when it is formatted as application/x-www-form-urlencoded — the format HTML forms submit and the format most server frameworks assume when parsing a query string. In that context a + decodes to a space. Everywhere else — anywhere in the path, or in a query string that is not form-encoded — a + is a literal plus sign and a space must be written as %20. Because %20 is unambiguous in every component of the URL, encodeURIComponent() always emits %20 for a space, never +.

Should I use encodeURI or encodeURIComponent in JavaScript?

Use encodeURIComponent() for individual pieces — a single path segment, a query parameter name, or a query parameter value — because it encodes the reserved delimiters (/ ? & = #) that would otherwise break the URL structure. Use encodeURI() only when you have an entire, already-structured URL and just want to make it valid by escaping spaces and non-ASCII characters; encodeURI() deliberately leaves / ? & = # untouched so it will not corrupt the URL's structure. The common bug is calling encodeURIComponent() on a whole URL (which mangles the ://) or encodeURI() on a value (which fails to escape & and = inside it).

How do I encode a forward slash inside a URL path?

Encode it as %2F. A raw / is always interpreted as a path-segment separator, so if a single identifier legitimately contains a slash — a filename like "My/Report" or a user handle like "John/Admin" — you must percent-encode it to %2F so the server treats it as one value instead of two path segments. encodeURIComponent("John/Admin") returns "John%2FAdmin". Note that some servers reject or normalise %2F in the path for security reasons, so a value with embedded slashes is often better carried as a query parameter.

Which characters never need to be encoded in a URL?

The RFC 3986 "unreserved" set is safe unencoded in every part of a URL: the letters A–Z and a–z, the digits 0–9, and the four symbols hyphen (-), period (.), underscore (_), and tilde (~). Every other character is either reserved (has a structural meaning you must preserve or encode) or unsafe, and should be percent-encoded when it appears inside a value. encodeURIComponent() encodes everything except the unreserved set plus a few legacy exceptions (! ' ( ) *).

How do I encode non-ASCII or international characters in a URL?

First encode the character as UTF-8 bytes, then percent-encode each byte. The Euro sign (€) is the three UTF-8 bytes E2 82 AC, so it becomes %E2%82%AC. JavaScript's encodeURIComponent() does this UTF-8-then-percent step for you automatically, which is one more reason to prefer it over hand-rolled encoding. Modern browsers display the decoded Unicode in the address bar but transmit the percent-encoded form on the wire.

What is double-encoding and why is it a problem?

Double-encoding is percent-encoding a value that is already encoded, so the percent sign of %20 gets re-encoded to %2520 (because % itself becomes %25). The server decodes it once and gets %20 back as literal text instead of a space, so the value is wrong. It usually happens when two layers of code both "helpfully" encode the same value. The fix is to encode exactly once, at the single point where the raw value is inserted into the URL, and never encode a string that already contains percent-escapes.

Why is proper URL encoding a security issue, not just a formatting one?

Unencoded special characters let an attacker change the meaning of a URL. An un-encoded slash in a user-supplied identifier can enable path traversal (../etc/passwd); an un-encoded & or = can inject extra query parameters (HTTP parameter pollution); and encoded-then-decoded payloads are a classic way to smuggle SQL-injection or XSS characters past naive input filters. Correct encoding keeps every user value confined to a single field, but it is not a substitute for server-side validation — always decode, validate against an allow-list, and use parameterised queries regardless.

URL encodingpercent encodingquery stringsURL pathsweb security