Broken query strings and redirects are often just encoding mistakes. Here is how to handle them safely.
This article focuses on the practical side of the workflow, including where developers usually get stuck, what to verify before trusting the result, and how the topic shows up in day-to-day implementation work.
Why URLs Cannot Carry Arbitrary Text
A URL is not a free-form string. It is a structured identifier built from components like the scheme, host, path, and query, and each of those components is parsed by relying on a small set of characters having fixed meanings. A question mark separates the path from the query string, an ampersand separates one query parameter from the next, and a slash divides path segments. If you drop raw text into a URL that happens to contain one of those characters, the parser has no way to know whether you meant the structural character or a literal value.
URL encoding, also called percent-encoding, exists to resolve exactly this ambiguity. It gives you a way to represent any byte as a sequence that the parser will treat as data rather than structure. The mechanism is simple: take the byte, express it as two hexadecimal digits, and prefix it with a percent sign. A space becomes %20, an ampersand becomes %26, a question mark becomes %3F. The original meaning is preserved, but the character can no longer interfere with how the URL is broken apart.
This matters because URLs travel through many systems: browsers, proxies, web servers, logging pipelines, and application code. Each one expects the structural characters to mean what the specification says they mean. Encoding is the contract that keeps all of those layers in agreement about where one piece ends and the next begins.
Reserved Versus Unreserved Characters
The relevant standard, RFC 3986, splits characters into two broad groups. Unreserved characters are safe to use literally anywhere in a URL: the uppercase and lowercase letters, the digits zero through nine, and four punctuation marks, the hyphen, period, underscore, and tilde. These never need encoding, and encoding them anyway is allowed but pointless.
Reserved characters are the ones that carry structural meaning in at least one part of a URL. This set includes the slash, question mark, hash, square brackets, at sign, ampersand, equals sign, plus sign, and several others. They are reserved precisely because the parser may interpret them as delimiters. When you want one of these characters to appear as literal data rather than as a delimiter, you must percent-encode it.
The subtlety is that a character can be reserved in one context and harmless in another. A slash is a delimiter inside a path but is just data inside a query parameter value, which is why a tool that encodes for one context may leave it untouched in another. Understanding which group a character belongs to, and in which part of the URL it appears, is the foundation for deciding what to encode and what to leave alone.
How Percent-Encoding Actually Works
Percent-encoding operates on bytes, not on characters directly, and that distinction becomes important the moment you leave plain ASCII. The process is: take the text, convert it to bytes using UTF-8, then replace every byte that is not an unreserved character with a percent sign followed by its two-digit hexadecimal value. A literal percent sign in your data must itself be encoded as %25, since the percent character is the escape marker.
For ASCII text the mapping is one byte per character, so a space (byte value 32, hex 20) becomes %20 and a comma (hex 2C) becomes %2C. For non-ASCII text each character may expand into multiple bytes, and therefore multiple percent-encoded triplets. The accented letter e with an acute accent encodes to %C3%A9 because its UTF-8 representation is two bytes. A typical emoji can expand to four percent-triplets.
Decoding reverses the procedure exactly: scan for percent signs, read the two hex digits that follow, reassemble the byte stream, and interpret it as UTF-8 to recover the original text. Because the operation is fully reversible, a correctly encoded value survives the round trip unchanged. Problems almost always come from encoding the wrong scope or running the process the wrong number of times, not from the algorithm itself.
Encoding a Whole URL Versus a Single Value
One of the most common mistakes is treating an entire URL and a single parameter value as if they need the same encoding. They do not, and conflating them produces broken links. If you encode a complete URL with a function meant for values, you destroy the structure, because the slashes, colon, and question mark that hold the URL together get turned into %2F, %3A, and %3F. The result is no longer a usable address.
When you are assembling a URL, you encode each dynamic piece individually before you splice it into the structural skeleton. The scheme, host, and path delimiters stay as literal characters; only the data you are inserting gets encoded. So if a user searches for the phrase fish and chips, you encode just that value to fish%20and%20chips and place it after the equals sign, leaving the surrounding scheme, the slashes, and the question mark intact.
The mental model that prevents errors is to think in terms of slots. The URL is a template with slots for values, and each slot gets its own independent encoding pass. You never encode the template, and you never paste an already-built URL into a slot meant for a single value. Keeping these two operations mentally separate eliminates a large class of bugs before they happen.
Why a Space Sometimes Becomes Plus Instead of %20
You will see spaces represented two different ways, as %20 and as a plus sign, and the reason is historical. The plus-as-space convention comes from the application/x-www-form-urlencoded format, the encoding HTML forms use when they submit data in a query string. In that specific format, a literal space is written as a plus sign, and a literal plus sign must therefore be encoded as %2B to avoid being read as a space.
The proper percent-encoding of a space, per the URL specification itself, is %20, and that form is valid everywhere in a URL, including the path. The plus convention is only meaningful inside the query string of a form submission. This creates a real interoperability trap: a value encoded as form data and then decoded by a generic URL decoder will leave plus signs as plus signs, silently turning your spaces into the wrong character.
Concretely, if a search box submits a phrase containing a plus sign and the server decodes it expecting form rules, it reads the space correctly. But if that same query string is processed by code that only understands percent-encoding, the user sees literal plus signs where spaces belonged. The safe habit is to know which decoder you are feeding and, when building URLs by hand, to prefer %20 because it is unambiguous in every context.
How Unencoded Characters Break Query Strings
The query string is where encoding failures show up most often, because it is built entirely from reserved delimiters. Parameters are separated by ampersands and each name is joined to its value by an equals sign. The instant a value contains one of those characters unencoded, the boundaries collapse and the parameter list is misread.
Imagine a redirect URL that carries a return address as a parameter, where the next value is itself a path with its own query string. Because the value contains a raw question mark and ampersand, the parser sees three separate things instead of one: a truncated next value, a stray parameter, and a tracking parameter that was meant to live inside the original address. The redirect lands on the wrong page, and the tracking is corrupted. Encoding the value first, so its question mark and ampersand become %3F and %26, keeps the whole thing inside a single parameter.
The same failure mode bites callback URLs in OAuth flows, signed links where an unencoded character changes the bytes used to compute a signature, and any value that legitimately contains an ampersand, such as a company name written with the symbol. Whenever a user-supplied or programmatically-generated value goes into a query string, it must be encoded as a unit, never trusted to be delimiter-free.
Double-Encoding and the Dreaded %2520
Double-encoding happens when a value that is already percent-encoded gets encoded a second time. Because the percent sign is itself a reserved character, the second pass encodes it, and %20 turns into %2520, the literal text that means percent-two-zero rather than a space. When the receiving system decodes only once, the user ends up with %20 visibly sitting in their data instead of a space.
This usually creeps in across layer boundaries. A front-end encodes a value, hands it to an API client that encodes the whole payload again, or a URL is encoded when stored in a database and re-encoded when read back out and placed into a link. Each layer assumes the data arrived raw and helpfully escapes it, and the corruption compounds. You can often spot the symptom in logs: a path that should read my file shows up as my%2520file, a dead giveaway that a percent sign was escaped one time too many.
The cure is to be deliberate about ownership: decide which single layer is responsible for encoding, and make every other layer treat the value as opaque and pass it through untouched. When debugging, decode the value repeatedly and count the passes. If it takes two decodes to reach readable text, you have a double-encoding bug, and the fix is to remove the redundant encode rather than to add a compensating decode.
encodeURIComponent Versus encodeURI in Practice
JavaScript ships two built-in functions for this job, and choosing the wrong one is a frequent source of subtle breakage. The function encodeURI is designed to encode a complete, already-structured URL, so it deliberately leaves the structural characters alone. It will not touch slashes, the question mark, the ampersand, the equals sign, or the hash, because in a full URL those characters are supposed to keep their meaning.
The function encodeURIComponent is designed to encode a single component, a parameter name or value, and it is far more aggressive. It encodes the slash, question mark, ampersand, equals sign, and hash, exactly the characters that would otherwise break a query string if they appeared inside a value. This is the function you want whenever you are inserting one piece of data into a larger URL you are assembling.
The practical rule is short: use encodeURIComponent for each individual value, and reserve encodeURI for the rare case where you are handed a whole URL that needs light cleanup. If you encode a value with encodeURI, an embedded ampersand survives unescaped and splits your query string; if you encode a full URL with encodeURIComponent, its slashes and colon get mangled into an unusable string. Match the function to the scope of what you are encoding and most encoding bugs simply disappear.