In Apache mod_rewrite, what happens to a request's original query string when a RewriteRule substitution contains a query string of its own, and what does the QSA flag change?
answer
- depends on whether the substitution has one
- a question mark means replace
- append versus replace versus discard
- QSA joins, QSD drops
- the parameters vanish with no error
basics
~20 sBy default the original query string survives only if the substitution has none of its own; a substitution containing a question mark replaces it. The QSA flag appends the original to the new one instead, and QSD discards it deliberately.
solid answer
~40 smod_rewrite's default is context-dependent, which is why this catches people out. If the substitution contains no `?`, the incoming query string is carried over untouched. If the substitution *does* contain a `?` — the common `\/index.php?id=$1` shape — that new query string replaces the original, and anything the client sent is silently dropped. `[QSA]`, "query string append", joins them instead, so `\/p/widget?ref=email` rewritten by `RewriteRule ^p/(.*)$ /index.php?item=$1 [QSA,L]` reaches the application as `item=widget&ref=email`. There is also `[QSD]` (httpd 2.4 and later) for the opposite intent: drop the original query string even though the substitution has none, which is how you strip tracking parameters. The failure this prevents is a quiet one — no error, no log entry, just parameters that were present at the edge and absent in the application.
code
apache · 13 linesRewriteEngine On
# Original query string preserved: substitution declares none.
RewriteRule ^p/(.*)$ /catalog/$1 [L]
# Original query string REPLACED: ?ref=email is silently lost.
RewriteRule ^q/(.*)$ /index.php?item=$1 [L]
# Both kept: item=widget&ref=email reaches the application.
RewriteRule ^r/(.*)$ /index.php?item=$1 [QSA,L]
# Deliberately stripped, e.g. to canonicalise before a cache.
RewriteRule ^legacy/(.*)$ /new/$1 [QSD,R=301,L]go deeper
Remember that a substitution containing its own ?parameters drops whatever the client sent, and that QSA is the flag that keeps both.
State the three cases precisely — preserved, replaced, appended — and know QSD as the deliberate discard, including why a bare trailing ? used to be the idiom.
Anticipate the production shape of the bug: tracking, pagination and return-URL parameters lost only for real traffic, plus duplicate keys after QSA resolving differently per framework.
Decide policy on which query parameters are allowed to reach origins and caches at all, since every parameter that survives usually widens the cache key and fragments hit rate.
## The rule, stated precisely There are three behaviours, and the default depends on the shape of your substitution. **Substitution has no query string.** The request's query string is passed through unchanged. ```apache RewriteRule ^p/(.*)$ /catalog/$1 [L] # /p/widget?ref=email -> /catalog/widget?ref=email ``` **Substitution has a query string.** It replaces the original entirely. ```apache RewriteRule ^p/(.*)$ /index.php?item=$1 [L] # /p/widget?ref=email -> /index.php?item=widget (ref is gone) ``` **Substitution has a query string, plus QSA.** The two are joined with `&`, substitution first. ```apache RewriteRule ^p/(.*)$ /index.php?item=$1 [QSA,L] # /p/widget?ref=email -> /index.php?item=widget&ref=email ``` The replacement case is where the bug lives. Nothing errors. The rule works perfectly for the URLs you tested by hand, because you typed them without a query string. It fails only for requests that carried parameters — which in practice means campaign tracking, pagination, filters, locale switches and return-URL parameters, arriving from real users rather than from your terminal. ## Why the default is what it is The design is defensible once you see it as "the substitution is authoritative about the query string if it says anything about it". A rule author who writes `?id=$1` has expressed an intention about what the query string should be; mod_rewrite honours it literally. A rule author who writes only a path has expressed no intention, so the existing one is preserved. `QSA` exists for the very common third case — "I have parameters of my own *and* I want the client's". Order matters if the same key appears on both sides. With `QSA`, the substitution's parameters come first and the original's follow, so `?item=widget&item=other` is possible. Which one wins is then a matter for whatever parses the query string in the application, not for httpd — PHP's `$_GET` takes the last occurrence, many other stacks take the first, and some expose both. If a duplicated key would be a security or correctness problem, do not rely on that resolution: strip or rename it deliberately. ## QSD, and the deliberate drop `[QSD]`, added in httpd 2.4.0, is the inverse. It discards the incoming query string even when the substitution has none: ```apache RewriteRule ^legacy/(.*)$ /new/$1 [QSD,R=301,L] # /legacy/x?utm_source=mail -> /new/x ``` Before `QSD` existed the idiom was to end the substitution with a bare `?`, which reads as an accident and is worth replacing when you meet it. `QSD` is the readable way to say "this endpoint takes no parameters" — useful for canonicalising URLs before a cache, since a query string that reaches a cache usually becomes part of the cache key and fragments the hit rate. ## The neighbouring flag: NE While you are reasoning about the tail of a URL, `[NE]` ("no escape") is the other flag that surprises people. mod_rewrite URL-escapes special characters in the substitution, so a literal `#` becomes `%23` and an `&` you intended as a separator can become `%26`. That is usually right, and occasionally exactly wrong — a redirect that must carry a fragment such as `/app#/section` needs `[NE]` or the client receives `%23` and lands nowhere useful. It matters mostly with `R`, since the escaped form ends up in the `Location` header. Treat `NE` with care: switching escaping off is the moment you become responsible for the safety of whatever you interpolate. Values captured from the request and re-emitted unescaped into a redirect target are how redirect and header-injection problems get introduced. If you need `NE`, constrain the pattern so the captured group cannot contain arbitrary input. ## How to confirm behaviour rather than guess The query string is easy to inspect from both ends. On the client, `curl -v 'https://example.com/p/widget?ref=email'` shows what you sent and, for a redirect, exactly what `Location` came back. On the server, `%{QUERY_STRING}` is available in `RewriteCond`, so you can branch on it, and the rewrite trace in the error log — raise `LogLevel` to a trace level, since httpd 2.4 removed `RewriteLog` — prints the substitution including its query string for each pass. Two minutes of that is worth more than reasoning about which default applies, especially in per-directory context where several rules may fire in sequence and only the last one's query-string handling is visible in the result.
- With QSA, what order do the parameters end up in, and what if a key appears on both sides?The substitution's own parameters come first, then the incoming ones, joined with `&`. A key present on both sides appears twice — `item=widget&item=other` — and mod_rewrite does not deduplicate. Which occurrence wins is decided by whatever parses the query string: PHP takes the last, several other stacks take the first. If a duplicate would be a correctness or security problem, strip or rename it explicitly rather than relying on the parser.
- What does the NE flag do, and when is it needed?NE stops mod_rewrite URL-escaping special characters in the substitution, so a literal `#` stays `#` instead of becoming `%23`. It matters chiefly with `R`, where the escaped form would otherwise be sent in the `Location` header — a redirect meant to carry a fragment needs it. Use it narrowly: with escaping off you own the safety of anything you interpolate, so constrain the captured group tightly.
- How would you make a rule behave differently depending on what the client sent in the query string?Match on `%{QUERY_STRING}` in a `RewriteCond`, since the RewriteRule pattern itself never sees the query string. For example a condition testing for `(^|&)debug=1(&|$)` can gate a rule that routes to a diagnostic handler. Remember the pattern and the condition are looking at different strings — path versus query — which is a frequent source of rules that appear correct and never fire.
saying these in an interview costs you the question
- Assumes the query string always survives a rewrite
- Thinks QSA replaces the original query string
- Adds QSA to a substitution that has no question mark
- Confuses QSA with QSD
- Expects the RewriteRule pattern to match the query string