skip to content

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?

level: middleimportance: nice to knowfreq 42%

answer

  1. depends on whether the substitution has one
  2. a question mark means replace
  3. append versus replace versus discard
  4. QSA joins, QSD drops
  5. the parameters vanish with no error

basics

~20 s

By 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 s

mod_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 lines
apache
RewriteEngine 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

for a junior

Remember that a substitution containing its own ?parameters drops whatever the client sent, and that QSA is the flag that keeps both.

for a middle

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.

for a senior

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.

for a principal

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

context