Nginx, trailing slashes and static files

Robert Crowther Dec 2022
Last Modified: Feb 2023

If you have not found the extensive web commentary, the problem is this—you are deploying static files. These files are public‐facing, not a page‐internal call, e.g for images. You would like nice URLs. You’ve set your Nginx config (I assume you know this much) and it’s working. But not perfectly. When you try to address the ‘index.html’ of a folder,

hidden.com/treasure

This is returned,

hidden.com/treasure/

Yep, the URL has been rewritten. A ‘curl’ will show it has been redirected.

Why is a slash here or there important?

If this was only about nice URLs, maybe not. But it has wider effects. First, what if the server talked about is fronted by another, maybe different, server? Proxys, load‐balancing and so forth? Confusion now in configuration and support. What if the front is a web‐platform? By not playing the way the platform needs, this issue could stall website availability. And then there is the effect on search‐engines. A site with sometimes, sometimes not, trailing‐slash handling will create a huge mess of search‐engine attempts to establish canonical URLs.

Scope of this article

This issue is wider, in results, than some writers are showing. Some coders are trying not to remove slashes, but to add them. Some coders are trying to adjust slashes for active code, not for static files. Those coders may get some help from this article, but will need to read and make their own way.

The problem of paths—which URLs are

Ok, this amusing article describes the base issue—I encourage you to have a look. If we talk about paths to files (which may be local as well as the web), this,

hidden.com/treasure

Is not the same as this,

hidden.com/treasure/

The first URL would return information about the ‘directory’ or ‘folder’ itself. The second would return the ‘contents’ of the folder. As the article amusingly trawls through, this applies in a different way to files. This,

hidden.com/treasure

Is not the same as this,

hidden.com/treasure.html

Is it? They are different files.

The writer of the linked article flashes impatience,

I just want the content, I want it to look slick—and the URL is a part of that.

I’d back him up. I’d go further. URLs are a path‐based locating system. Yes, back to the dawn of time there has been for URLs a ‘display the directory contents’ system but, if that goes out to the public, it’s regarded nowadays as a fail (in that respect, this site is either weird, or abstract). So the ‘nice’ URL is a different system. It’s contents‐only.

What happens in Nginx? Why?

Yes, but Nginx must handle not only the public ‘nice URL’ form, but forms for LDAP servers, load‐balancing scenarios, and so forth. With it’s usual most‐obvious decisiveness, Nginx insists on trailing slashes. It’s answer to,

hidden.com/treasure

Is to redirect, very publicly, to,

hidden.com/treasure/

To me, this is consistent, reasonable, and the external redirect will avoid the possibility of system‐crashing internal‐redirect loops. But it doesn’t meet the needs of a ‘nice’ URL scheme. Can it be made to do that?

What has Nginx got?

Not enough people talk about this. Because they are trying to patch‐fix? You’ll find the story in Nginx documentation of static file handling online. Quick, there is a ‘root’ directive. This asks Nginx to look in the declared location for static files,

location / {
  root     WebPages/treasure/html;
}

The ‘root’ may be defaulted by Nginx install routines—on a Linux machine a root default may be, for example, ‘/usr/local/var/www/’. If a file does not exist, ‘root’ will also search for an ‘index’ file. And if that fails, it returns ‘500 Server Error’.

Then there is a ‘try_files’ directive. This asks Nginx to try alternatives to a map of file directory structure. Note that the ‘$uri’ variable contains everything after the domain,

location / {
  # If file search fails, use the 'default' image
  try_files  $uri default.svg;
}

’try_files’ can also throw a custom error,

location / {
  # If file search fails, throw a 'not found'
  try_files  $uri =404;
}

Useful to know, the error directive throws an internal redirect, so if you declared a custom 404 error within scope,

error_page 404 /404.html;

Nginx request processing will return that.

Nginx directive interaction

You can see that the ‘root’ directive is a comprehensive solution. It looks for files, falls back to an index, and errors with a ‘500 Server Error’. If you are happy with URLs following the file structure, ‘try files’ is not necessary.

‘try_files’ is for poking about in deployed directories, and for more custom errors. There’s no change in behaviour by adding,

location / {
  try_files  $uri;
}

because that says, go look for a file at, say,‘root’ + ‘cask/potions/amber‐stew’,

WebPages/treasure/html/cask/potions/amber-stew

A ‘root’ declaration will do that anyway.

That hasn’t helped!

True. Configurations of the above are scattered through examples on web‐posts. All the configurations will, when Nginx receives a URL, go look for files. But, however configured, Nginx static‐file processing is consistent. If Nginx finds a directory, from a URL without a trailing slash, it will throw a redirect. It must use,

hidden.com/cask/potions/

not

hidden.com/cask/potions

to access, for example,

WebPages/treasure/html/cask/potions/index

Solutions

Like I say, the trailing‐slash issue covers many situations, not one. So I’ve seen several solutions,

Switch off absolute redirect

This is an unusual solution. Relative rewrites directive will not insert trailing slashes and server port marks,

server {
  absolute_redirect off;
...
}

Advantages: this may solve several tailing slash issues. And work across websites with no modification. Disadvantage: may introduce different behaviour to aspects of Nginx processing.

URL regex capture

I can’t be bothered to illustrate this. Oh, ok, go ahead then. You set up some awesome statement like,

server {
  ...
  # Remove trailing slash from URLs
  rewrite ^/(.*)/$ /$1 permanent;

  location / {
    ...
  }
}

Yuch. But it will work.

Use of ’try_files’

Could claim this is similarly bonkers, but it works from documentation,

location / {
  # Ensure directory indexes are tried
  # without requiring trailing slashes
  try_files  $uri $uri/index =404;
}

What? Yes, it says, if nothing matches the URL‐without‐slash, try look for ’$uri + slash + index’. Which will succeed, without throwing a redirect. Does this affect web‐root? Amusingly, no.

Let’s step forward, to the general case. The only issue with this is that a subdirectory will continue to accept a trailing slash,

hidden.com/treasure/

will also succeed. However, in terms of search‐engines, and compared to where this post started, that is a small issue.

Refs

Direct writing describes the base of the issue,

https://christopheraue.net/design/urls-without-trailing-slash-or-extension

Nginx documentation on ‘root’ and ‘try_files’,

https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/