The common mistake is misunderstanding the fact that the last parameter of the try_files
directive is completely different from all the previous ones. While all the earlier parameters mean "check whether the corresponding file or directory exists relative to the current web server root", the last parameter (or more precisely, one of its possible interpretations depending on what's specified there) is a so-called fallback URI. If none of the checks succeed, nginx performs what's known as an internal redirect using this fallback URI, and request processing starts over from one of the very first request processing phases.
Whether a try_files
parameter is checked as a file or a directory depends solely on whether it ends with a trailing slash. When using a construction like try_files $uri $uri/ ...
, it doesn't matter whether the value contained in the $uri
variable itself ends with a slash — what matters is whether the trailing slash is explicitly present in the argument passed to the try_files
directive.
Now, what happens when using the directive try_files $uri /index.html /index.php
? First, it checks whether the requested URI corresponds to a physical file under the web server root. If it doesn't (e.g., when the request URI is /directory
), it then checks whether the index.html
file exists under the web root (as we clarified during the discussion in the SO comments, that file doesn't exist). Finally, it performs an internal redirect to /index.php — potentially losing the query string, if one was present.
Is the try_files
directive needed here at all? The try_files
directive handler doesn't look for index files, whether the processed parameter is checked as a file or as a directory — that's the responsibility of the index
directive handler, which is invoked during the next request processing phase. The try_files
directive itself has almost nothing to do with how the response is actually generated. Only if your intention is to route any request that doesn't match a physical file or directory to your main index.php
script, you do need a try_files
directive like this:
try_files $uri $uri/ /index.php$is_args$args;
Otherwise, you don't need this directive at all (though the try_files $uri =404;
directive within the PHP location handler makes sense), and the minimal working configuration for you will look like
root /var/www/vhosts/example.org/web; index index.php index.html; location / {} location ~ \.php$ { fastcgi_pass unix:/var/www/vhosts/system/example.org/php-fpm.socket; # fastcgi_index index.php; <-- doesn't make sense here include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; try_files $uri =404; }
TL;DR
So, how this configuration actually works? It's pretty obvious that a request like http://example.com/index.php
will be handled by the second location. But why is a request like http://example.com/
also handled there? After all, the original request URI doesn't match the "\.php$"
regex pattern, and the first location doesn't have any PHP handler; it's only declared in the second one.
In fact, if we had written something like:
try_files $uri $uri/index.php /index.php$is_args$args;
then a request to http://example.com/
would have returned the raw index.php
file source, because it would've been processed by the first location, which (implicitly) uses what's called the static content handler to generate the response. And yet, with the original config, the request to http://example.com/
is processed correctly. Why?
When the request processing reaches the phase responsible for the response content generation, and there's no explicitly defined content handler (like proxy_pass
, fastcgi_pass
, etc.) in the current location, a chain of default modules for this phase is activated. One of the first modules in that chain is the ngx_http_index_module
.
Let's take a closer look at the documentation for that module, especially the following fragment:
It should be noted that using an index file causes an internal redirect, and the request can be processed in a different location. For example, with the following configuration:
location = / { index index.html; } location / { ... }
a /
request will actually be processed in the second location as /index.html
.
Yes, if the current request URI maps to a physical directory under the web server root, and there's a matching index file in it, the index
directive will perform an internal redirect, appending the name of the index file to the current request URI. To be more precise, the logic of the index module works roughly like this:
If the request URI does not end with a slash, the module refuses to process the request and passes control to the next handler in the content phase handler chain. Later, if the request reaches the static content handler and it determines that the URI maps to a physical directory rather than a file, a 301 Moved Permanently
redirect will be issued to the same URI with a trailing slash appended.
The module searches for a suitable index file in the physical directory that corresponds to the request URI (using the file list defined in the index
directive):
If a suitable index file is found, an internal redirect is performed to the current URI plus the index file name.
If, during the index file existence check, the module discovers that the directory itself does not exist, a 404 Not Found
error is returned.
If none of the index files is found, but the last parameter of the index
directive is a file with an absolute path (e.g., /index.php
), then an internal redirect is performed using that path as a new URI.
If none of the above match, and no internal redirect is triggered (or error is returned), the module hands control off to the next handler in the content phase registered handlers chain.
This behavior of the index
directive explains why the request to http://example.com/
is handled correctly in the example config above. The index
directive, when processing the /
request, finds the index.php
file in the server root directory, and performs an internal redirect to /index.php
. As a result, the second location block is selected to process the new URI, and the request is passed to ngx_http_fastcgi_module
as the content handler.
All of the above should explain why your original configuration without the $uri/
parameter in the try_files
directive wasn't working properly — if the requested URI maps to a physical directory, the request never reaches the index
directive handler, which would have selected the most appropriate index file in the requested directory and performed the necessary internal redirection.
If the try_files
directive had included the $uri/
parameter, the check for whether the request matches a physical directory relative to the web server root would have succeeded, and further request processing would have been passed to the index
directive handler. Then:
If the incoming request looked like /directory
, a permanent redirect to the /directory/
URI would have been returned.
If the incoming request looked like /directory/
, the index
directive handler would have attempted to locate an index file within that directory. Once the index file was found, it would have performed an internal redirect to the same URI with the index file name appended. If the index file name ended with .php
, the corresponding PHP handler location would have been used to process the new URI.
It should also explain why your configuration
location = /survey { rewrite ^/survey?$ /survey/index.php; }
works, while
location = /survey { rewrite ^/survey?$ /survey/index.php break; }
does not. By using the break
flag, you're instructing nginx to continue processing the rewritten URI within the current location block — which, by default, uses the static content handler. On the other hand, when you omit the break
flag, the matched rewrite
triggers a new search for the best matching location based on the rewritten URI, which in this case allows nginx to route the request properly (e.g., to the PHP handler).