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:

nginx
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:

nginx
rewrite ^/page$ /new-page? last;  # Trailing ? discards query string

Step 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:

bash
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.