You added a rewrite rule to redirect old URLs to new ones, but nothing happens. Or worse, the rule works but causes a redirect loop. Or maybe it redirects to the wrong URL entirely. Nginx rewrite rules can be tricky—they interact with location blocks, have subtle regex differences from Apache, and the order matters more than you'd expect.
Let's debug rewrite rule issues systematically.
Understanding Rewrite Rule Syntax
Nginx rewrite syntax:
rewrite regex replacement [flag];The flags are:
- last - Stop processing, start new search for location
- break - Stop processing, continue in current location
- redirect - 302 temporary redirect
- permanent - 301 permanent redirect
A common mistake is mixing up Apache and Nginx syntax:
```nginx # Apache .htaccess style (won't work in Nginx) RewriteRule ^old-page$ /new-page [R=301,L]
# Correct Nginx syntax rewrite ^/old-page$ /new-page permanent; ```
Step 1: Test Your Regex Pattern
The most common issue is an incorrect regex. Nginx uses PCRE syntax:
```bash # Test regex pattern echo "/old-page" | grep -P "^/old-page$"
# Use pcretest for complex patterns pcretest PCRE> /^\/old-page$/ PCRE> /old-page 0: /old-page ```
Common regex mistakes:
```nginx # Wrong: Missing anchors rewrite old-page /new-page; # Matches /anything-old-page-anything
# Correct: Anchored pattern rewrite ^/old-page$ /new-page permanent; # Matches exactly /old-page
# Wrong: Not escaping dots rewrite ^/old.page$ /new-page; # . matches any character
# Correct: Escaped dot rewrite ^/old\.page$ /new-page permanent; ```
Step 2: Check Rewrite vs Return
For simple redirects, use return instead of rewrite:
```nginx # Using rewrite (works but less efficient) rewrite ^/old-page$ /new-page permanent;
# Using return (preferred for simple redirects) location = /old-page { return 301 /new-page; }
# For domain redirects server { server_name old.example.com; return 301 https://new.example.com$request_uri; } ```
return is clearer, faster, and less error-prone for simple redirects.
Step 3: Understand Location Block Processing
Rewrite rules in different contexts behave differently:
```nginx # Server-level rewrite - runs before location matching server { rewrite ^/old-page$ /new-page permanent;
location / { # This sees the rewritten URL } }
# Location-level rewrite server { location /old { rewrite ^/old-page$ /new-page last; # 'last' re-runs location matching }
location /new { # Request ends up here } } ```
Order matters:
```nginx # Wrong: General location matches first location / { # This catches everything, including /old-page }
location = /old-page { rewrite ^ /new-page permanent; } ```
The = exact match has highest priority, so that would work. But prefix locations are matched in order of specificity:
```nginx # Both are prefix matches, first matching wins for equal specificity location /old { # This catches /old, /old-page, /old-anything }
location /old-page { # Never reached for /old-page because /old matched first } ```
Step 4: Debug with Nginx Logging
Enable rewrite logging:
```nginx server { error_log /var/log/nginx/error.log notice; rewrite_log on;
location / { rewrite ^/old-page$ /new-page last; } } ```
Test and check the log:
```bash # Make a test request curl -I http://localhost/old-page
# Check the rewrite log tail -f /var/log/nginx/error.log ```
Output shows the rewrite process:
``
2026/04/04 12:00:00 [notice] 1234#1234: *1 rewritten "/old-page", new: "/new-page"
Step 5: Fix the Last vs Break Flag
The difference between last and break is crucial:
```nginx # 'last' - Re-run location matching with new URL location /old { rewrite ^/old/(.*)$ /new/$1 last; # After rewrite, Nginx searches for matching location again }
location /new { # This is matched after rewrite }
# 'break' - Continue in current location location /files { rewrite ^/files/(.*)$ /data/$1 break; # After rewrite, Nginx stays in this location # and serves the file from /data/ try_files $uri =404; } ```
Common mistake:
```nginx # Wrong: Using 'last' breaks proxy_pass location /api { rewrite ^/api/(.*)$ /$1 last; proxy_pass http://backend; # Never reached because 'last' re-runs location matching }
# Correct: Use 'break' to stay in this location location /api { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://backend; } ```
Step 6: Fix Redirect Loops
Redirect loops happen when a rewrite creates a URL that matches itself:
```nginx # This causes a loop location /page { rewrite ^/page$ /page permanent; # Redirects to itself }
# Common mistake with trailing slashes location /dir { rewrite ^/dir$ /dir/ permanent; # /dir -> /dir/ -> /dir/ ... }
# Fix: Be more specific location = /dir { rewrite ^ /dir/ permanent; }
location /dir/ { # Handle /dir/ here } ```
Debug loops by checking the redirect chain:
```bash # Follow redirects curl -L -v http://localhost/old-page 2>&1 | grep -E "(< HTTP|< Location)"
# Limit redirect count curl -L --max-redirs 5 http://localhost/old-page ```
Browser shows:
``
ERR_TOO_MANY_REDIRECTS
Step 7: Fix Query String Handling
Query strings require special attention:
```nginx # Query string is NOT matched by rewrite regex # URL: /page?param=value rewrite ^/page$ /new-page; # Query string is preserved automatically
# Result: /new-page?param=value
# To capture query string rewrite ^/page\?(.*)$ /new-page?$1? last; # WRONG - query string not in regex
# Correct way to modify query string if ($args ~ param=(\w+)) { set $param $1; rewrite ^/page$ /new-page?newparam=$param? last; }
# Or using map map $arg_param $new_param { default ""; ~^(.+)$ $1; }
server { location /page { rewrite ^/page$ /new-page?param=$new_param? last; } } ```
To discard query string:
rewrite ^/page$ /new-page? last; # Trailing ? discards query stringStep 8: Handle Special Characters
URL encoding can cause issues:
```nginx # Spaces in URLs # URL: /old page rewrite "^/old page$" /new-page permanent; # Quotes required for spaces
# Or use encoded form rewrite ^/old%20page$ /new-page permanent;
# Regex special characters need escaping # URL: /file.php?id=1 rewrite ^/file\.php /new-file; # Escape the dot
# Capture groups # URL: /product/123 rewrite ^/product/(\d+)$ /item?id=$1 last; ```
Test with encoded URLs:
```bash # URL encode a string python3 -c "import urllib.parse; print(urllib.parse.quote('old page'))" # Output: old%20page
# Test the rewrite curl -v "http://localhost/old%20page" ```
Step 9: Check for Conflicting Rules
Multiple rewrite rules can conflict:
```nginx # Problem: Both rules match /page rewrite ^/page$ /new-page permanent; rewrite ^/p(.*)$ /other$1 last; # This also matches
# Nginx processes rewrites in order, first match wins # But the first rule redirects, so user never sees the second
# Fix: Order matters, or use location blocks location = /page { return 301 /new-page; }
location /p { rewrite ^/p(.*)$ /other$1 last; } ```
Use nginx -T to see all rules in order:
nginx -T 2>&1 | grep -A 2 "rewrite"Step 10: Debug with Debug Log
For complex issues, enable debug logging:
```nginx server { error_log /var/log/nginx/error.log debug;
location / { rewrite_log on; rewrite ^/old-page$ /new-page last; } } ```
The debug log shows every step:
```bash # Make request curl http://localhost/old-page
# Watch detailed log tail -f /var/log/nginx/error.log | grep -E "(rewrite|location|test)" ```
Common Rewrite Patterns
Here are correctly implemented common patterns:
```nginx # WWW to non-WWW server { server_name www.example.com; return 301 https://example.com$request_uri; }
# HTTP to HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; }
# Remove trailing slash (except root) rewrite ^/(.+)/$ /$1 permanent;
# Add trailing slash to directories rewrite ^/([^.]*[^/])$ /$1/ permanent;
# Old path to new path location /old-path { rewrite ^/old-path/(.*)$ /new-path/$1 permanent; }
# Remove file extension location / { if (-f $request_filename.html) { rewrite ^(.*)$ $1.html break; } }
# WordPress-style pretty URLs location / { try_files $uri $uri/ /index.php?$args; }
# Proxy with path rewrite location /api/ { rewrite ^/api/(.*) /$1 break; proxy_pass http://backend; } ```
Quick Debugging Checklist
When a rewrite isn't working:
```bash # 1. Test the regex echo "/your-url" | grep -P "^your-regex$"
# 2. Check Nginx syntax nginx -t
# 3. Enable rewrite logging # Add: rewrite_log on; and error_log ... notice;
# 4. Test the URL curl -I http://localhost/your-url
# 5. Check the logs tail -f /var/log/nginx/error.log
# 6. Verify location priority nginx -T 2>&1 | grep -E "(location|rewrite)"
# 7. Check for redirect loops curl -Lv http://localhost/your-url 2>&1 | grep "< Location" ```
Verification
After fixing your rewrite rules:
```bash # Test syntax nginx -t
# Reload systemctl reload nginx
# Test the specific rewrite curl -I http://localhost/old-url # Should show: HTTP/1.1 301 Moved Permanently # Or: HTTP/1.1 302 Found
# Test with curl following redirects curl -L http://localhost/old-url
# Verify final destination curl -w "%{url_effective}\n" -L -o /dev/null -s http://localhost/old-url ```
Rewrite rules in Nginx are powerful but require understanding of the processing order, regex syntax, and the difference between last and break flags. When debugging, always enable rewrite logging and test with curl to see exactly what Nginx is doing.