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
| Part | Address used in this test | Sent to the server |
|---|---|---|
| scheme | http | Decides the port and whether traffic is encrypted |
| host | 127.0.0.1 | Yes |
| port | 8198 | Yes; when omitted the protocol default applies, 80 for http and 443 for https |
| path | /a/b/c | Yes |
| query | x=1&y=2 | Yes, in its encoded original form |
| fragment | section-3 | No |
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 #.