503 Maintenance Mode, nginx, CORS, POST

It took me a while to mix “maintenance mode”, CORS preflight responses, and nginx into a coherent picture. It was painful, friend. If you have the same problem I hope you read on and save yourself the trouble.

What are we dealing with here? We have a HTTP API which serves data to a browser–based client. Our API is proxied by nginx, and nginx also handles CORS responses. We’d like to place a file on our appserver which causes nginx to stop proxying requests, and instead start issuing 503 Service Unavailable responses—a “maintenance mode”.

One of the first things you’ll find when trying to get such a mode working is this snippet:

if (-f $document_root/maintenance.html) {
  return 503;
}

error_page 503 @maintenance;
location @maintenance {
  rewrite ^(.*)$ /maintenance.html break;
}

This looks pretty good. We get the proper HTTP 503 status code, and some human readable text—whatever we included in the maintenance.html file. Although there’s the nebulous IfIsEvil, we’re using a return which is one of the safe statements [1].

However, any API requests which necessitate CORS checks—especially preflighting—aren’t going to fly here (har har). We’ll need some extra headers:

if (-f $document_root/maintenance.html) {
  return 503;
}

error_page 503 @maintenance;
location @maintenance {
  add_header 'Access-Control-Allow-Origin' '*';
  rewrite ^(.*)$ /maintenance.html break;
}

Great, almost time for the pub. Let’s just check that a typical API request works:

$ curl -X POST -i https://api.example.org/

HTTP/1.1 405 Not Allowed
Server: nginx
# snip

Fiddlesticks. So we cop a 405 Not Allowed rather than a 503 Service Unavailable because our POST is internally redirected to a static file (maintenance.html). nginx rightfully isn’t happy about us retrieving a static with such a verb.

As far as I can tell, the only way to modify the request verb within nginx is to proxy the request. This requires something to proxy to, so we create a new named location and upstream:

error_page 503 @maintenance;
location @maintenance {
    proxy_method GET;
    proxy_pass http://maintenance_upstream;
}

upstream maintenance_upstream {
  server unix:/path/to/socket;
}

server {
  listen unix:/path/to/socket;
  server_name _;
  root /path/to/statics/;
  add_header 'Access-Control-Allow-Origin' '*';
  rewrite ^(.*)$ /maintenance.html;
}

Now we can serve 503 responses to POST requests too.

$ curl -X POST -i https://api.example.org/

HTTP/1.1 503 Service Unavailable
Server: nginx
# snip

One last nicety—we’re serving human-readable HTML to describe our maintenance status, but how about something for the machines too? Let’s modify the “maintenance server” to switch on the Accept header.

map $http_accept $maintenance_filename {
  default "maintenance.html";
  ~application/json "maintenance.json";
}

server {
  listen unix:/path/to/socket;
  server_name _;
  root /path/to/statics/;
  add_header 'Access-Control-Allow-Origin' '*';
  rewrite ^(.*)$ /$maintenance_filename;
}

Where maintenance.json contains something like:

{
  "status": "maintenance",
  "detail": "Engineers are investigating a
             hovercraft/eel capacity issue"
}

Now we have the ability to communicate maintenance status to API clients, and to give some detail about it.

[1]nginx totally fails the principle of least surprise check here. There’s a good explanation of what’s going on at http://agentzh.blogspot.com.au/2011/03/how-nginx-location-if-works.html