Why NGINX returns 405 for a POST with a 504 GATEWAY TIMEOUT, and other Gotchas around its error page directive

NGINX' error_page directive keeps me busy, again. This week at engageSPARK we noticed a seemingly weird behavior by NGINX: On POST requests, NGINX would sometimes return a 405 METHOD NOT SUPPORTED response. This is weird, to say the least: Either your web application (the upstream server) supports POST or it does not—supporting it sometimes does not make any sense.

While investigating this, I stumbled over a few gotchas. Maybe they'll help others, too.

Root cause: 504 GATEWAY TIMEOUT

As it turns out, sometimes the upstream server takes too long to process requests. In the cases, when we encountered a 405, we actually found lines like this in the error log:

nginx_1  | 2017/06/02 13:27:36 [error] 7#7: *1 upstream timed out (110: Connection timed out) while reading response header from upstream, client:, server: localhost, request: "POST / HTTP/1.1", upstream: "", host: "localhost:8080"

Connection timed out is the key here. The upstream server simply took longer than NGINX was willing to wait. Relevant configuration options to change the timeout: fastcgi_read_timeout, proxy_read_timeout and in rarer circumstances: proxy_connect_timeout, proxy_send_timeout, (respectively for FastCGI).

This error really should result in a 504 GATEWAY TIMEOUT. Why does NGINX return a 405 METHOD NOT SUPPORTED instead? Because static files cannot handle POST requests. (Huh, what?)

Gotcha: Static files cannot handle POST requests

As mentioned on the mailing list, static files cannot handle POST requests—only HEAD and GET is applicable. Which makes sense, from the standpoint that NGINX will only deliver files, not modify them, as the other VERBs such as POST imply.

Where are you modifying a static file? When you configure an error handler and use a named location, for example:

error_page 500 = @error_page;

location @error_page {
  root /usr/syno/share/nginx;
  rewrite (.*) /error.html break;

The solution is to use an internal location instead of a named one:

error_page 500 /error_page;

location /error_page {
    root /usr/syno/share/nginx;
    rewrite (.*) /error.html break;

Wait, why would this change anything regarding the method? Because, sometimes it does. :)

Gotcha: error_page changes the method to GETsometimes (not for named locations)

As the docs for error_page state:

This causes an internal redirect to the specified uri with the client request method changed to “GET” (for all methods other than “GET” and “HEAD”).

And then a bit later about named locations:

If there is no need to change URI and method during internal redirection it is possible to pass error processing into a named location

Meaning, if you use a named location for your error, then an error during a POST request will be redirected as a POST request to the named location.

If you use any other way to define your error handler, then a GET will be used instead.

As stated above: you can GET static files, but you can't POST to them.

Another error you may run into is the following, for example when you define an error handler for the 405 and wonder why it's being igonored.

Gotcha: recursive_error_pages is off by default

When you encounter an error while handling a previous error, your error_page directive for the new error is ignored by default.

You can change this by enabling recursive_error_pages:

server {
# …
    recursive_error_pages on;
# …

The error_page directive is potentially harmful—you can easily get into an internal redirect loop when an error of a kind occurs while you try to handle an error of the same kind. For example, if you encounter a 500 INTERNAL SERVER ERROR while handling a 500 already, you could run into an endless loop. To avoid this, I presume, the default is off.

This is not mentioned in the docs for error_page, but only in the recursive_error_pages, when you arguably are already aware of the problem:

Enables or disables doing several redirects using the error_page directive. The number of such redirects is limited.

Gotcha: location error handlers disable all server handlers

Defining any error handlers on a location block disables inheritance of all error_page directives from the server block.

For example:

server {

  error_page 500 @err504;

  location / {
    error_page 409 @err409;

In the above example, let's assume a 500 error occurs while processing requests for the location /. In this case, the default handler would kick in, and the error_page directive internally redirecting to @err504 would be ignored. Because defining an error handler for 409 cleaned the slates for all status codes.