• Removing Slashes from a URL in NGINX

    One green gate stands open in a stone garden arch, while a removed matching gate rests against the wall.

    /page/1.htm and //page//1.htm/ might mean the same page in your application. If the first is the canonical URL, redirect the second to it. But a rule that removes every trailing slash can send a perfectly ordinary directory into a redirect loop.

    There are two separate decisions here: how NGINX matches a request, and which URL the browser should display.

    What NGINX Already Normalizes

    NGINX enables merge_slashes by default. It compresses adjacent slashes in the URI used for location matching. A request for //page//1.htm can therefore match location = /page/1.htm without a custom rewrite.

    That does not send a redirect. The browser still has the original URL.

    The $request_uri variable holds the original URI, including the query string; $uri holds the current normalized path. Normalization also decodes %XX sequences and resolves . and .. components. Comparing those variables does not isolate duplicate slashes, so blindly putting $uri into a redirect is a broader transformation than it looks.

    Redirect a Known Route

    Suppose /page/1.htm is a static file and the application deliberately uses that URL without a trailing slash. The following standalone server block handles only that route’s aliases. Put it inside the http context and replace the document root with your own:

    nginx
    server {
        listen 8080;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;
    
        merge_slashes on;
    
        location = /page/1.htm {
            if ($request_uri != "/page/1.htm$is_args$args") {
                return 301 /page/1.htm$is_args$args;
            }
    
            try_files $uri =404;
        }
    
        location = /page/1.htm/ {
            return 301 /page/1.htm$is_args$args;
        }
    
        location / {
            try_files $uri $uri/ =404;
        }
    }

    The exact locations match normalized paths. The first redirects only when the original URL differs from the canonical spelling; an already canonical request reaches the file. The second handles the trailing-slash alias. Even //page//1.htm/ reaches the final URL in one redirect.

    Both destinations use a fixed path. No decoded path fragments are copied into the response. $is_args$args preserves a nonempty query string, including its encoded values. Encoded spellings and dot components that normalize to this route also become aliases; do not apply this policy to signed URLs or routes where the original spelling matters.

    The if block contains only return, which stops processing and sends the redirect. These are page URLs intended for GET and HEAD. Keep API endpoints and form submissions outside this rule; redirect status and method preservation need their own decision.

    Leave Directory Slashes Alone

    For a real directory, /docs/ is a useful canonical URL. NGINX’s static file handler redirects /docs to /docs/ before serving its index.

    A server-wide rewrite ^/(.*)/$ /$1 permanent; does the opposite. Together they produce /docs/ → /docs → /docs/, forever. The example above leaves directory handling intact instead of imposing one slash policy on every URL.

    Check the Redirect Chain

    With a file at page/1.htm and an index at docs/index.html, the expected results are:

    Request Expected result
    /page/1.htm 200, no redirect
    //page//1.htm/ One 301 to /page/1.htm, then 200
    //page//1.htm/?q=a%2Fb&n=1 One 301 to /page/1.htm?q=a%2Fb&n=1, then 200
    /docs One 301 to /docs/, then 200
    /docs/ 200, no redirect
    /missing 404

    Check syntax with nginx -t, then inspect the responses on a local test server:

    bash
    curl --path-as-is -I 'http://localhost:8080//page//1.htm/?q=a%2Fb&n=1'
    curl --path-as-is -sS -L --max-redirs 3 -o /dev/null \
        -w 'status=%{http_code} redirects=%{num_redirects} url=%{url_effective}\n' \
        'http://localhost:8080//page//1.htm/?q=a%2Fb&n=1'

    Repeat for the other rows and a path containing %20. --path-as-is keeps curl from resolving dot components before sending a test request. If NGINX sits behind a TLS-terminating proxy, also check that the Location header uses the public scheme, host and port. Use temporary 302 redirects during rollout testing before committing clients to cacheable 301 responses.