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:
- The rewrite rule catches any URL ending with / and redirects
- try_files attempts to serve index.html when the path is a directory
- 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:
- Request comes for
/foo/bar/
- Nginx removes the slash via rewrite (301 to
/foo/bar
) try_files
finds/foo/bar/index.html
exists- Nginx internally redirects to
/foo/bar/index.html
- 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;
}
}