Page caching vs. RESTful routes in Rails
GWW is almost six years old. It began life on Rails 1. It’s picked up on many of the improvements to Rails made since then, but not all of them. One upgrade that remains to be done is to take more advantage of Rails’ RESTful routing. (Yes, that’s a link to the Rails 2.x docs; GWW is waiting on a Phusion Passenger bug to move to Rails 3.)
I recently added a new model type and set of page to GWW — it now explicitly tracks ScoreReports — and took the opportunity to begin using RESTful routes. It worked beautifully; I really appreciate being able to lay down a whole set of standard, named routes with so little code. But when I pushed the score reports tracking feature to production, it broke: creating a new score report (done by pushing a button to submit a form) did nothing, only refreshed to the index view with no new score report at the top. After a little head-scratching and watching the form submission with Live HTTP Headers, I realized that, like seemingly every only-in-production bug, the problem was due to caching.
In production, GWW runs under Apache httpd and Phusion Passenger, which despite the bug I mentioned have been rock-solid. Many of GWW’s pages take a little time to generate, so to speed things up most of them are cached using Rails’ page-caching mechanism. So that cached pages don’t litter the public
directory as they would in the default configuration, GWW puts them in public/cache
with this line in config/environments/production.rb
:
config.action_controller.page_cache_directory = File.join(RAILS_ROOT, "public/cache")
Passenger needs no configuration to serve cached pages from the default location in public
, but having moved them they must be redirected to manually in public/.htaccess
:
RewriteEngine On # If REDIRECT_STATUS is empty (not set), this request comes directly from a # client, not from an internal redirect following one of the rewrites below. # In that case, forbid URIs which begin with "cache" so clients can't request # cached pages directly from the cache directory. RewriteCond %{ENV:REDIRECT_STATUS} ^$ RewriteRule ^cache - [F,L] RewriteCond %{DOCUMENT_ROOT}/cache/index.html -f RewriteRule ^$ cache/index.html [L] RewriteCond %{DOCUMENT_ROOT}/cache%{REQUEST_URI}.html -f RewriteRule ^(.+)$ cache/$1.html [L]
This worked fine until GWW acquired its first RESTful route.
Under the old Rails routing conventions (or at least GWW’s interpretation of them), the URI from which one would get a list of score reports would be something like /score_reports/list
and the URI to which one would post to create a ScoreReport would be something like /score_reports/create
. With RESTful routes, both actions use the same URI, /score_reports
, one with HTTP method GET (exercised by clicking on a link in a browser) and one with HTTP method POST (exercised by submitting a form).
The bug had been in the caching configuration all along; it was just exposed for the first time by adding to the application two routes that differed only in HTTP method. The redirect to a cached page wasn’t considering the HTTP method. GETting /score_reports
in the browser cached the page, then POSTing to /score_reports
caused httpd to return the cached page before the request ever reached Rails.
Once understood the problem was easily fixed by adding a condition to rewrite only GETs:
RewriteEngine On # If REDIRECT_STATUS is empty (not set), this request comes directly from a # client, not from an internal redirect following one of the rewrites below. # In that case, forbid URIs which begin with "cache" so clients can't request # cached pages directly from the cache directory. RewriteCond %{ENV:REDIRECT_STATUS} ^$ RewriteRule ^cache - [F,L] RewriteCond %{REQUEST_METHOD} GET RewriteCond %{DOCUMENT_ROOT}/cache/index.html -f RewriteRule ^$ cache/index.html [L] RewriteCond %{REQUEST_METHOD} GET RewriteCond %{DOCUMENT_ROOT}/cache%{REQUEST_URI}.html -f RewriteRule ^(.+)$ cache/$1.html [L]
With this configuration in place POSTing to /score_reports
routes to ScoreReportsController.create
as intended. In web applications in general it never makes sense to cache POSTs, PUTs or DELETEs, so GWW’s caching configuration would certainly have been this way in the first place if it had occurred to me. Oh well; better late than never.
Now that the first set of RESTful routes is working, I’ll look forward to converting more of GWW to use them. The tighter routes.rb
is, the less work there will be to do when GWW moves to Rails 3.
Leave a Reply