Request type and nginx caching

A few weeks ago, I published a new post and was immediately contacted by Aaron Brazell reporting that the page was blank. A few moments of testing couldn’t reproduce the issue before it “resolved itself,” so I attributed his trouble to some transient problem and thought little more of it. After all, I’d received just one inquiry about this over the last several months of regular publishing.

I should’ve investigated further, as the problem proved quite easy to reproduce.

For context, one of the many performance steps I’ve taken was to configure nginx’s ngx_http_fastcgi_module module (related: “Planning for the post that Matt links to”). As part of that setup, I specified the cache key nginx should use to store and retrieve individual requests. As I learned recently, the cache key format I chose could cause nginx to cache empty output under specific circumstances.

Request types interfere

When connecting to a remote resource, there are several different types of request that can be made to the server that hosts the resource in question. For most who read this post on ethitter.com, your browser will issue a GET request to my server to retrieve the content. If one submits a form, say to search this site, the browser sends a POST request. These are the two most-common request types, and both were properly handled by my initial cache configuration. A third request type–and several more thereupon–however, was unaccounted for.

In many circumstances, it is useful for me to check the status of a page without loading it in a browser. I may have changed a header recently and need to confirm that the updates were correct. I could have moved a page and added a redirect that I need to verify. If I’ve recently published a post, I may wish to check that it is being served from the nginx cache as expected. In each of these cases, I would use a HEAD request to obtain the information I need. Unfortunately, these requests were causing nginx to serve blank pages given the right circumstances.

Reproducing this problem was quite simple once I understood its cause. To do so, I just needs to use a cache key that didn’t include the request type, and issue a HEAD request before any successful GET request for an uncached resource. In this scenario, since the HEAD request never produced page content, nginx cached an empty body along with the headers it retrieved. Since my configuration used very short cache lives (microcaching), the issue appeared transient, even though it was readily recreated.

A painfully-simply resolution

Once I’d identified why blank pages were cached, correcting this mistake was quite simple: I only needed to incorporate the request type into the cache key. Below are the cache key I was previously using, followed by its corrected counterpart.

fastcgi_cache_key "$scheme://$host$request_uri";
fastcgi_cache_key "$request_method|$scheme://$host$request_uri";

Simply prefixing the cache key with $request_method| ensures that the various data returned by different request types doesn’t pollute the caches for subsequent requests of other types.