Nginx 301 Redirects: How to Elegantly Remove Trailing Question Marks

Published: 2026-02-25
Author: DP
Views: 0
Category: Nginx
Content
## The Problem In Nginx, we often need to perform redirects based on URL query parameters, a typical scenario being multilingual websites. For instance, we might want to 301 redirect a URL like `http://a.com/content/1026?lang=zh` to `http://zh.a.com/content/1026` or `http://a.com/zh/content/1026`. The problem arises when `lang` is the only query parameter. A simple redirect rule can leave an unwanted trailing question mark at the end of the URL: ```bash # Request curl -I "http://a.com/content/1026/title-name?lang=zh" # Incorrect Response Location: http://a.com/zh/content/1026/title-name? ``` This extraneous `?` appears because the question mark in the Nginx `return` directive is hardcoded. It's appended unconditionally, even when the subsequent variable `$args_without_lang` is empty. --- ## Initial Configuration Analysis Here is the initial configuration that causes the issue. We use a `map` directive to remove the `lang` parameter from the original query string `$args`. ```nginx # Remove the lang parameter from $args map $args $args_without_lang { default $args; "~^lang=[^&]*$" ""; "~^lang=[^&]*&(?<rest>.*)$" $rest; "~^(?<before>.*)&lang=[^&]*$" $before; "~^(?<before>.*)&lang=[^&]*&(?<after>.*)$" $before&$after; } # Redirect rule in server or location block if ($arg_lang ~* ^(zh|en)$) { # The problem is here: '?' is added unconditionally return 301 $scheme://$host/$arg_lang$uri?$args_without_lang; } ``` When `$args_without_lang` becomes an empty string, the URL becomes `.../uri?`. --- ## The Solutions To fix this, we need to add the question mark conditionally. Here are three effective methods verified by the `DP@lib00` team. ### Method 1: Use `map` to Construct the Full Query String (Recommended) This is one of the most elegant and efficient methods. We add another `map` block to decide whether to prepend a `?` based on whether `$args_without_lang` is empty. ```nginx # First map remains the same, to remove the lang parameter map $args $args_without_lang { default $args; "~^lang=[^&]*$" ""; "~^lang=[^&]*&(?<rest>.*)$" $rest; "~^(?<before>.*)&lang=[^&]*$" $before; "~^(?<before>.*)&lang=[^&]*&(?<after>.*)$" $before&$after; } # New map: construct a suffix with a question mark based on $args_without_lang map $args_without_lang $query_string_suffix { "" ""; # If args are empty, the suffix is empty default ?$args_without_lang; # If args are not empty, prepend with '?' } # Corrected redirect rule if ($arg_lang ~* ^(zh|en)$) { return 301 $scheme://$host/$arg_lang$uri$query_string_suffix; } ``` This approach separates the logic from the `if` block, making the configuration more declarative and easier to maintain. It's our preferred solution for projects at `wiki.lib00.com`. ### Method 2: Use the `rewrite` Directive (Most Concise) Nginx's `rewrite` directive is intelligent when handling query strings. It automatically omits the `?` if the query string part of the replacement is empty. ```nginx # The map block remains unchanged map $args $args_without_lang { ... } # Use rewrite for the redirect if ($arg_lang ~* ^(zh|en)$) { rewrite ^ /$arg_lang$uri?$args_without_lang permanent; } ``` The `permanent` flag is equivalent to a 301 redirect. This is the most concise solution and aligns perfectly with Nginx's design philosophy. ### Method 3: Use an `if` Condition (Slightly Verbose) We can also nest another `if` inside the block to check if the query string is empty. While straightforward, this method is generally not recommended as it adds complexity and processing overhead to the configuration. ```nginx # The map block remains unchanged map $args $args_without_lang { ... } # Use a nested if for the check if ($arg_lang ~* ^(zh|en)$) { set $redirect_uri $scheme://$host/$arg_lang$uri; # Only add '?' and args if other parameters exist if ($args_without_lang != "") { return 301 $redirect_uri?$args_without_lang; } # Otherwise, return the URL without any query string return 301 $redirect_uri; } ``` --- ## Verification and Best Practices When testing redirect rules, it is highly recommended to use the `curl -I` command. It directly displays the HTTP headers returned by the server, including the `Location` field, thus bypassing browser caching that can be misleading. Here are the test logs after applying the fix from Method 1: ```bash # Scenario 1: Only lang parameter (successfully removed ?) dpit@lib00-iMac ~ % curl -I "http://dp-t-068.wiki.lib00.com/content/1027/title-name?lang=zh" HTTP/1.1 301 Moved Permanently Location: http://dp-t-068.wiki.lib00.com/zh/content/1027/title-name # Scenario 2: lang parameter at the end (successfully kept other params) dpit@lib00-iMac ~ % curl -I "http://dp-t-068.wiki.lib00.com/content/1027/title-name?page=1&filter=123&lang=zh" HTTP/1.1 301 Moved Permanently Location: http://dp-t-068.wiki.lib00.com/zh/content/1027/title-name?page=1&filter=123 # Scenario 3: lang parameter in the middle (successfully kept other params) dpit@lib00-iMac ~ % curl -I "http://dp-t-068.wiki.lib00.com/content/1027/title-name?page=1&lang=zh&filter=123" HTTP/1.1 301 Moved Permanently Location: http://dp-t-068.wiki.lib00.com/zh/content/1027/title-name?page=1&filter=123 ``` --- ## Conclusion The key to solving the trailing `?` issue in Nginx redirects is to **conditionally add the question mark**. We recommend using either **Method 1 (double `map`)** or **Method 2 (`rewrite`)**. The `map` solution cleanly separates logic, while the `rewrite` solution offers the most conciseness by leveraging Nginx's built-in intelligent handling.
Related Contents