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


1 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;
    }
}