Nginx 301 Redirects: How to Elegantly Remove Trailing Question Marks
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
The Ultimate Beginner's Guide to Regular Expressions: Master Text Matching from Scratch
Duration: 00:00 | DP | 2025-12-02 20:47:30How Can a Docker Container Access the Mac Host? The Ultimate Guide to Connecting to Nginx
Duration: 00:00 | DP | 2025-12-08 23:57:30Nginx vs. Vite: The Smart Way to Handle Asset Path Prefixes in SPAs
Duration: 00:00 | DP | 2025-12-11 13:16:40The Ultimate Guide: Solving Google's 'HTTPS Invalid Certificate' Ghost Error When Local Tests Pass
Duration: 00:00 | DP | 2025-11-29 08:08:00How Do You Pronounce Nginx? The Official Guide to Saying It Right: 'engine x'
Duration: 00:00 | DP | 2025-11-30 08:08:00The Ultimate Nginx Guide: How to Elegantly Redirect Multi-Domain HTTP/HTTPS Traffic to a Single Subdomain
Duration: 00:00 | DP | 2025-11-24 20:38:27The SEO Dilemma: Is `page=1` Causing a Duplicate Content Disaster?
Duration: 00:00 | DP | 2025-11-26 06:44:42PHP Regex Optimization: How to Merge Multiple preg_replace Calls into One Line
Duration: 00:00 | DP | 2026-01-21 08:24:30The Ultimate Vue SPA SEO Guide: Perfect Indexing with Nginx + Static Generation
Duration: 00:00 | DP | 2025-11-28 18:25:38Modular Nginx Configuration: How to Elegantly Manage Multiple Projects with Subdomains
Duration: 00:00 | DP | 2025-11-29 02:57:11Can robots.txt Stop Bad Bots? Think Again! Here's the Ultimate Guide to Web Scraping Protection
Duration: 00:00 | DP | 2025-11-09 08:15:00Multilingual SEO Showdown: URL Parameters vs. Subdomains vs. Subdirectories—Which is Best?
Duration: 00:00 | DP | 2025-11-12 11:51:00Nginx Redirect Trap: How to Fix Incorrectly Encoded Ampersands ('&') in URLs?
Duration: 00:00 | DP | 2025-12-31 11:34:10How to Add Port Mappings to a Running Docker Container: 3 Proven Methods
Duration: 00:00 | DP | 2026-02-05 10:16:12Step-by-Step Guide to Fixing `net::ERR_SSL_PROTOCOL_ERROR` in Chrome for Local Nginx HTTPS Setup
Duration: 00:00 | DP | 2025-11-15 15:27:00Why Does My Nginx + PHP-FPM Seem Single-Threaded? Unmasking the PHP Session Lock
Duration: 00:00 | DP | 2025-11-15 23:51:00The Ultimate MinIO Docker Deployment Guide: From Public Access to Nginx Reverse Proxy Pitfalls
Duration: 00:00 | DP | 2026-02-24 16:57:16Recommended
Python String Matching Mastery: Elegantly Check for Multiple Prefixes like 'go' or 'skip'
00:00 | 49How can you efficiently check if a string in Pytho...
The Ultimate Guide to Seamlessly Switching from Baidu Tongji to Google Analytics 4 in Vue 3
00:00 | 44Switching from Baidu Tongji to Google Analytics (G...
From Guzzle to Native cURL: A Masterclass in Refactoring a PHP Translator Component
00:00 | 43Learn how to replace Guzzle with native PHP cURL f...
How to Add Port Mappings to a Running Docker Container: 3 Proven Methods
00:00 | 14In development and operations, it's a common scena...