How to Properly Remove Trailing Slashes in Nginx URLs Without Infinite Redirect Loops


24 views

When configuring Nginx to handle URLs with and without trailing slashes, many developers encounter the dreaded infinite redirect loop. Let's examine why this happens with the standard approach:

# Problematic configuration
rewrite ^(.+)/$ $1 permanent;
index index.html;
try_files $uri $uri/index.html =404;

The issue occurs because Nginx processes directives in a specific order:

  1. The rewrite rule catches any URL ending with / and redirects
  2. try_files attempts to serve index.html when the path is a directory
  3. The rewritten URL gets reprocessed through the same rules

Here's the proper way to handle trailing slashes while serving static content:

server {
    listen 80;
    server_name example.com;
    
    root /usr/share/nginx/www/example.com;
    
    # Serve files with or without trailing slash
    location / {
        # Only redirect if URI ends with slash and isn't just "/"
        if ($request_uri ~ ^/(.*)/$) {
            return 301 /$1;
        }
        
        try_files $uri $uri/ $uri/index.html =404;
    }
    
    # Additional configurations...
}

This solution avoids the redirect loop by:

  • Using if condition carefully (normally discouraged but safe here)
  • Checking $request_uri instead of $uri in the condition
  • Excluding the root path (/) from redirection
  • Proper ordering of try_files directives

Verify your setup works correctly with these test cases:

curl -I http://example.com/foo/bar      # Should return 200
curl -I http://example.com/foo/bar/     # Should return 301 to /foo/bar
curl -I http://example.com/foo/bar.html # Should return 200 or 404

For high-traffic sites, consider these optimizations:

# Cache the redirect decisions
map $request_uri $trailing_slash_redirect {
    default 0;
    ~^/(.*)/$ /$1;
}

server {
    # ... other config ...
    
    if ($trailing_slash_redirect) {
        return 301 $trailing_slash_redirect;
    }
}

When working with static sites in Nginx, many developers want clean URLs without trailing slashes. The typical approach using rewrite ^(.+)/$ $1 permanent; combined with try_files $uri $uri/index.html =404; often creates infinite redirect loops. Here's why:

# Current problematic config snippet
rewrite ^(.+)/$ $1 permanent;
index index.html;
try_files $uri $uri/index.html =404;

The loop occurs because:

  1. Request comes for /foo/bar/
  2. Nginx removes the slash via rewrite (301 to /foo/bar)
  3. try_files finds /foo/bar/index.html exists
  4. Nginx internally redirects to /foo/bar/index.html
  5. But index.html is the index file, so Nginx serves /foo/bar/ again

Here's the correct configuration that handles all three URL cases:

server {
    # ... other server config ...
    
    # Handle directory requests without trailing slash
    location ~ /$ {
        return 301 $scheme://$host$uri;
    }

    # Main handling
    location / {
        try_files $uri $uri/ $uri/index.html =404;
        
        # Remove trailing slash for non-directory requests
        if ($request_uri ~ ^/(.*)/$) {
            return 301 /$1;
        }
    }
}

For those who prefer regex-only solutions:

server {
    # ... other config ...
    
    # Remove trailing slash for non-directory requests
    if ($request_uri ~ ^/(.*)/$) {
        set $path $1;
        if (-d $document_root/$path) {
            rewrite ^/(.*)/$ /$1 permanent;
        }
    }

    try_files $uri $uri/ $uri/index.html =404;
}

Verify the behavior with these test cases:

  • /foo/bar → Serves /foo/bar/index.html without redirect
  • /foo/bar/ → 301 redirect to /foo/bar
  • /foo/bar/index.html → 301 redirect to /foo/bar
  • /actual-file.html → Serves directly without redirect

When implementing this solution:

  • The if directive has performance implications in Nginx - use sparingly
  • For high-traffic sites, consider using map directives instead
  • Always test with nginx -t before reloading configuration

Here's a full configuration that implements this solution:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    
    # Remove trailing slash except for directories
    if ($request_uri ~ ^/(.*)/$) {
        set $no_slash $1;
        if (!-d "$document_root/$no_slash") {
            return 301 /$no_slash;
        }
    }
    
    location / {
        try_files $uri $uri/ $uri/index.html =404;
    }
    
    # Special handling for actual directories
    location ~ /$ {
        try_files $uri $uri/index.html =404;
    }
    
    error_page 404 /404.html;
    location = /404.html {
        internal;
    }
}