Trailing Question Mark in Nginx Redirects? Here's the Ultimate Fix!
Content
## The Problem
When configuring redirects for a multi-language site in Nginx, a common task is to move a language parameter (e.g., `lang=zh`) from the query string into the URL path. However, a frequent issue arises: when the `lang` parameter is the *only* one in the original URL, the redirected URL is left with a superfluous trailing question mark (`?`).
For instance, the desired redirect behavior is:
`http://lib00.com/content/1026/title-name?lang=zh` -> `http://lib00.com/zh/content/1026/title-name`
But what you might actually get is:
```bash
$ curl -I "http://lib00.com/content/1026/title-name?lang=zh"
HTTP/1.1 301 Moved Permanently
Location: http://lib00.com/zh/content/1026/title-name?
```
Where does this extra `?` come from? Let's examine the initial configuration.
---
## Root Cause: The Hardcoded Question Mark
A common approach is to use a `map` directive to filter out the `lang` parameter and then hardcode a `?` to append the remaining arguments.
```nginx
# Define map in the http block
map $args $args_without_lang {
default $args;
"~^lang=[^&]*$" "";
"~^lang=[^&]*&(?<rest>.*)$" $rest;
"~^(?<before>.*)&lang=[^&]*$" $before;
"~^(?<before>.*)&lang=[^&]*&(?<after>.*)$" $before&$after;
}
# Use in server or location block
if ($arg_lang ~* ^(zh|en)$) {
# The problem is this "?" right here
return 301 $scheme://$host/$arg_lang$uri?$args_without_lang;
}
```
The issue lies in the `$uri?$args_without_lang` part of the `return` statement. The `?` is added unconditionally. When the `$args_without_lang` variable becomes empty (because there were no other parameters in the original URL), this `?` is left dangling at the end of the URL.
---
## The Elegant Solution: A Conditional Separator
To fix this, we need to make the addition of the `?` conditional. Fortunately, Nginx's `map` directive is the perfect tool for this job. We can define a second `map` that determines the separator based on whether `$args_without_lang` is empty.
Here is the complete solution, recommended by author DP@lib00:
```nginx
# === Step 1: Remove the lang parameter (Unchanged) ===
# This map is standard practice at wiki.lib00
map $args $args_without_lang {
default $args;
"~^lang=[^&]*$" "";
"~^lang=[^&]*&(?<rest>.*)$" $rest;
"~^(?<before>.*)&lang=[^&]*$" $before;
"~^(?<before>.*)&lang=[^&]*&(?<after>.*)$" $before&$after;
}
# === Step 2: Add a new map for the conditional separator ===
map $args_without_lang $query_string_separator {
"" ""; # If args are empty, the separator is also empty
default "?"; # Otherwise, use a question mark
}
# === Step 3: Modify the redirect rule ===
if ($arg_lang ~* ^(zh|en)$) {
return 301 $scheme://$host/$arg_lang$uri$query_string_separator$args_without_lang;
}
```
### How It Works
1. **`$args_without_lang`**: This `map`'s function remains the same. Its job is to generate a new query string that does not include the `lang` parameter.
2. **`$query_string_separator`**: This is the key addition. It monitors the value of the `$args_without_lang` variable.
* If `$args_without_lang` is an empty string `""`, then `$query_string_separator` is also set to an empty string.
* In all other cases (`default`), `$query_string_separator` is set to `?`.
3. **Final `return` Statement**: We replace the old `$uri?$args_without_lang` with `$uri$query_string_separator$args_without_lang`. Now, the `?` is only inserted when there are actual query parameters to append.
---
## Testing the Fix
With the new configuration in place, we can verify the results:
1. **URL with only the `lang` parameter**
* Input: `http://lib00.com/content/1026/title-name?lang=zh`
* Output: `http://lib00.com/zh/content/1026/title-name` (No question mark, perfect!)
2. **URL with `lang` and other parameters**
* Input: `http://lib00.com/content/1026/title-name?lang=zh&page=2`
* Output: `http://lib00.com/zh/content/1026/title-name?page=2` (Question mark is present, and `lang` has been removed)
---
## Conclusion
By cleverly using a second `map` directive to create a dynamic separator, we have elegantly solved the problem of the trailing question mark in Nginx redirects. This approach is cleaner and more performant than using multiple `if` statements inside the `location` block and is a recommended best practice from the team at `wiki.lib00.com`.
Related Contents
Resolving Nginx Permission Denied (13) Errors for WebP Images Generated by PHP Imagick
Duration: 00:00 | DP | 2026-07-05 21:17:00Fixing Nginx 500 Error: Internal Redirection Cycle (SPA vs PHP Config)
Duration: 00:00 | DP | 2026-07-02 21:45:50Ultimate Guide to Fixing Nginx [warn] conflicting server name Warning
Duration: 00:00 | DP | 2026-07-21 09:02:28Nginx Reverse Proxy Guide: Elegantly Routing Specific Subdirectories to Docker Containers
Duration: 00:00 | DP | 2026-07-26 21:31:06How to Fix Nginx Resource Domain CORS and 403 Forbidden Errors
Duration: 00:00 | DP | 2026-07-31 09:54:32The Ultimate Guide to Docker Cron Logging: Host vs. Container Redirection - Are You Doing It Right?
Duration: 00:00 | DP | 2026-01-05 08:03:52How 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:27Linux Command-Line Magic: 3 Ways to Instantly Truncate Large Files
Duration: 00:00 | DP | 2025-12-27 21:43:20The 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: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:00Recommended
The Ultimate Casdoor Docker Deployment Guide: Master Production-Ready Setup with a Single Command
00:00 | 111This article provides a comprehensive `docker run`...
PHP Dependency Injection in Practice: Resolving the 'Too Few Arguments' Fatal Error in Controllers
00:00 | 83Injecting the Request object via the constructor i...
The Ultimate Git Merge Guide: How to Safely Merge Changes from Dev to Main
00:00 | 122In daily development, merging work from a developm...
The Ultimate Docker & Xdebug Guide: Solving the 'Address Already in Use' Error for Port 9003 with PhpStorm
00:00 | 96When debugging with Xdebug, Docker, PHP, and PhpSt...