Notes › Tech Notes › Which part of a URL reaches the server? Six parts, %20, + and double encoding

Which part of a URL reaches the server? Six parts, %20, + and double encoding

The anchor a page jump carried is nowhere in the log, the Chinese search term turns into a string of percent signs in the log, and the plus sign in a key silently becomes a space. All three point to the same source: a URL is not sent to the server in one piece; it is cut into six parts, and each part meets a different fate.

Where the six parts are

PartAddress used in this testSent to the server
schemehttpDecides the port and whether traffic is encrypted
host127.0.0.1Yes
port8198Yes; when omitted the protocol default applies, 80 for http and 443 for https
path/a/b/cYes
queryx=1&y=2Yes, in its encoded original form
fragmentsection-3No

Test 1: the fragment never leaves the browser

I started a minimal server locally and printed the fields it received verbatim. Requesting http://127.0.0.1:8198/a/b/c?x=1&y=2#section-3, the server recorded REQUEST_URI as /a/b/c?x=1&y=2 and QUERY_STRING as x=1&y=2, parsed x=1 and y=2, and section-3 appeared nowhere. So a missing fragment in the backend log is not a dropped field; it never goes over the network. In-page navigation and hash-based client routing are handled by the browser alone.

Test 2: a plus can mean a space, but not the other way around

In the same batch, q=a+b+c and q=a%20b%20c both parse to a b c on the server, while token=a%2Bb parses to a literal a+b and token=a+b gives "a b". The difference is that query string decoding treats a plus as another way to write a space, so a real plus has to be sent as %2B. Concatenate one directly and it becomes a space; plenty of "the key is mysteriously wrong" investigations end on this single character.

Test 3: Chinese and double encoding

Request q=%E4%B8%AD%E6%96%87 and QUERY_STRING shows the encoded original while the parsed parameter is already Chinese. If the same string is encoded twice, the address becomes q=%25E4%25B8%25AD, the server decodes once and gets %E4%B8%AD, still carrying percent signs, which means only one layer came off. There is no mystery here; the only reliable fix is to count your decoding: the transport layer decodes once, and if the application layer needs another pass it has to be agreed explicitly.

Test 4: duplicate keys, values without keys, invalid escapes

Repeated key names do not turn into an array by themselves. ?q=a&q=b parses down to the last value b; to receive both you have to write q[]=a&q[]=b before the server returns an array of two items. A fragment with only a value and no key is even more blunt: ?=onlyvalue&k= parses to a single empty k, and the lonely onlyvalue simply disappears, unreachable from any code.

Encoding can hide in key names too. In ?a=1&%20b=2&c=%20 the server decodes the keys as a and b, with the leading space eaten, while the trailing %20 in a value honestly becomes a space and stays. Invalid forms do not raise errors either: q=100% and q=a%zzb reach the server as-is, and the parsed result is still 100% and a%zzb. That only shows this local parser is forgiving; a percent sign is supposed to be closed by two hex digits, and when it is a bare % you cannot assume every middleware along the chain will let it through the same way. To send a literal percent sign, write %25; in testing q=100%25 parses to 100%.

Which characters must be escaped in a query value

The ones to watch are the characters that carry structural meaning: & is treated as a separator, = as the key-value divider, + as a space, # truncates the whole query on the spot, and % starts every escape. When I sent q=%3D%2B%26%25, the server decoded =+&%, four characters exactly, which means one decode restores it fully. Slashes need no attention: ?path=/x/y passes through unchanged.

Path segments follow another set of rules: once # appears as itself, everything after it stays in the browser; written as %23 it is just three characters in the path, and in testing /a%23b/c?q=1 reached the server with REQUEST_URI still /a%23b/c and the query parsed as usual. Likewise %2F in /a%2Fb/c travels to the server still encoded, and whether it is decoded into an extra path separator is up to each framework's routing rules. That is exactly why "should a slash in a path be escaped" cannot be answered by guessing.

Form bodies and query strings are not the same habit

When submitting as application/x-www-form-urlencoded, the encoding rules in the body match the query string and a space is again written as +; switch to a JSON body and the string itself carries these characters, with no percent escaping needed. Bring the query string habit into JSON and you store dirty data containing %20. The same Chinese text has to be written %E4%B8%AD%E6%96%87 in a query, while JSON only needs correct UTF-8.

The easy way to split one apart

When debugging I paste the whole address into URL Parser, let it list the six parts, and read the raw QUERY_STRING next to the parsed parameters; when I need to put Chinese or special characters into a value by hand, I generate the escape with URL Encoder and Decoder rather than typing percent signs by eye, which always goes wrong once it gets long.

Q: Are Chinese characters encoded the same way in a path and in a query

Both use percent encoding, but the allowed character sets differ: several characters may appear as-is in a path segment, while a query string has to escape the equals sign and ampersand that act as separators. Handing the whole string to a standard encoding function is safer than judging character by character.

Q: Why does the log store the encoded form

Because the access log records the raw QUERY_STRING, and decoding happens when the framework parses parameters. In the same line of test output above, the two fields are the raw encoding and the decoded result, so you can compare them directly.

Q: Can the backend count page changes in a hash-routed app

No, the fragment never leaves the browser. To count by page you need each view to have its own path or query parameters, which is the main reason frontend routing moved from hash mode to history mode.

Q: Should page state live in the hash or the query

It depends on whether it needs to leave the browser. Changing the hash sends no request, does not enter server logs and is not treated as a different page by crawlers, which suits purely client-side expansion, positioning and pagination cursors; once the value must be read by the server, such as filter conditions, share links or analytics dimensions, it has to go into the path or the query, because in testing REQUEST_URI never contained anything after the #.

📎 Related articles

PNG compressed three times only lost 426 bytes: the real boundary of lossless compression 2026-10-22 How Many Times Bigger a PNG Gets After Converting From WebP: When It Is Unavoidable and How to Keep the Growth Down 2026-10-22 Three Reasons the Small Text Smudges When a Screenshot Is Shrunk to a Quarter 2026-10-22 Cropped to the Same 300x300, Why Does the Avatar Always Sit Half a Cell Low-Right 2026-10-22 Forecasting 18 months at 1.7 times a month: why the script and the sheet give two numbers 2026-10-22