http://www.heise.de
or http://www.spiegel.de
several
times a day to read the latest news, then certain elements of that page
(for example, the logo image at the top of the page) will not be loaded
again on your second visit. Your browser already has these files in the
cache, which saves loading time and bandwidth.If-Modified-Since:
header. The web server can then
compare this information to the corresponding file and either deliver a
newer version or return an HTTP 304 Not Modified code as response. In
case of a 304, the web server delivers the cached version. Now you are
going to say, "That's all very well for images, but it won't help me at
all for dynamically generated web pages such as the Index view of the
companies." Ah, but you are underestimating what Rails can do.
;-)index
and
show
methods in the controller file
app/controllers/companies_controller.rb
as follows
:def index @companies = Company.order(:id) fresh_when last_modified: @companies.maximum(:updated_at) end def show @company = Company.find(params[:id]) fresh_when last_modified: @company.updated_at end
@companies = Company.order(:id)
instead of
@companies = Company.all
in order to be able to use
ActiveRecord's lazy loading (see the section called “Lazy Loading”).http://0.0.0.0:3000/companies
:$ curl -I http://0.0.0.0:3000/companies HTTP/1.1 200 OK Last-Modified: Fri, 13 Jul 2012 12:14:50 GMT [...] $
Last-Modified
entry in the HTTP header was
generated by fresh_when
in the controller. If
we later go to the same web page and specify this time as well, then we
do not get the web page back, but a 304 Not Modified
message:$ curl -I http://0.0.0.0:3000/companies --header 'If-Modified-Since: Fri, 13 Jul 2012 12:14:50 GMT' HTTP/1.1 304 Not Modified Last-Modified: Fri, 13 Jul 2012 12:14:50 GMT Cache-Control: max-age=0, private, must-revalidate X-Ua-Compatible: IE=Edge X-Request-Id: 7802f078add46dc372adaec92f343fe2 X-Runtime: 0.008647 Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-04-20) Date: Fri, 13 Jul 2012 14:27:15 GMT Connection: close $
Started HEAD "/companies" for 127.0.0.1 at 2012-07-13 16:29:53 +0200 Processing by CompaniesController#index as */* (0.2ms) SELECT MAX("companies"."updated_at") AS max_id FROM "companies" Completed 304 Not Modified in 2ms (ActiveRecord: 0.2ms)
@companies.maximum(:updated_at)
in the controller. We only
had to check when the last update was done in the database. As soon as a
single company record changes, the value is set to the then current time
and the whole web page is delivered once more. With this method, you can
also deliver generically generated web pages via
Last-Modified
headers.update_at
field of a particular
object is not meaningful on its own. For example, if you have a web page
where users can log in and this page then generates web page contents
based on a role model, it can happen that user A as admin is able to see
an Edit link that is not displayed to user B as normal user. In such a
scenario, the Last-Modified header explained in the section called “Last-Modified” does not help.If-None-Match:
query to the web server.index
and
show
methods in the controller file
app/controllers/companies_controller.rb
as
follows:def index @companies = Company.all fresh_when etag: @companies end def show @company = Company.find(params[:id]) fresh_when etag: @company end
http://en.wikipedia.org/wiki/Cross_site_request_forgery
).
But it also means that each new user of a web page gets a new etag for
the same page. To ensure that the same users also get identical CSRF
tokens, these are stored in a cookie by the web browser and consequently
sent back to the web server every time the web page is visited. The curl
we used for developing does not do this by default. But we can tell curl
that we want to save all cookies in a file and transmit these cookies
later if a request is received.-c cookies.txt
parameter.$ curl -I http://0.0.0.0:3000/companies -c cookies.txt HTTP/1.1 200 OK Etag: "b5f711016cb2e5fce352230e607ceffe" Content-Type: text/html; charset=utf-8 Cache-Control: max-age=0, private, must-revalidate [...] $
-b cookies.txt
, curl sends these
cookies to the web server when a request arrives. Now we get the same
etag for two subsequent requests:$ curl -I http://0.0.0.0:3000/companies -b cookies.txt HTTP/1.1 200 OK Etag: "132c1be24595b9b5f7b2c08b300592b1" [...] $ curl -I http://0.0.0.0:3000/companies -b cookies.txt HTTP/1.1 200 OK Etag: "132c1be24595b9b5f7b2c08b300592b1" [...] $
$ curl -I http://0.0.0.0:3000/companies -b cookies.txt --header 'If-None-Match: "132c1be24595b9b5f7b2c08b300592b1"' HTTP/1.1 304 Not Modified Etag: "132c1be24595b9b5f7b2c08b300592b1" Cache-Control: max-age=0, private, must-revalidate [...] $
304 Not Modified
in response. Let's look at
the Rails log:Started HEAD "/companies" for 127.0.0.1 at 2012-07-13 18:45:38 +0200 Processing by CompaniesController#index as */* Company Load (0.3ms) SELECT "companies".* FROM "companies" Completed 304 Not Modified in 3ms (ActiveRecord: 0.3ms)
current_user
. The methods
index
and show
would
then look like this in the
app/controllers/companies_controller.rb
controller:def index @companies = Company.all fresh_when etag: [@companies, current_user] end def show @company = Company.find(params[:id]) fresh_when etag: [@company, current_user] end
Etag
and Last-Modified
together. Here is what it looks like:def index @companies = Company.order(:id) fresh_when :etag => @companies.all, :last_modified => @companies.maximum(:updated_at) end def show @company = Company.find(params[:id]) fresh_when @company end
@company
has a method
updated_at
. This is then used automatically by
fresh_when
.Employee
is edited or
deleted? Then the show view and potentially also the index view would
have to change as well. That is the reason for the line belongs_to :company, :touch => truein the employee model. Every time an object of the class
Employee
is saved in edited form, and if
:touch => true
is used, ActiveRecord updates the
superordinate Company
element in the database.
The updated_at
field is set to the current time. It
is "touched".fresh_when
and
then do without the respond_to do |format|
block. But HTTP
caching is not limited to HTML pages. Yet if we render JSON (for
example) as well and want to deliver it via HTTP caching, we need to use
the method stale?
. Using
stale?
resembles using the method
fresh_when
. The example of the section called “Combining Etag
and Last-Modified” would then look like
this if we use stale?
and additionally render
JSON:def index @companies = Company.order(:id) if stale? :etag => @companies.all, :last_modified => @companies.maximum(:updated_at) respond_to do |format| format.html format.json { render json: @companies } end end end def show @company = Company.find(params[:id]) if stale? @company respond_to do |format| format.html format.json { render json: @company } end end end
public: true
in
fresh_when
or stale?
.
The example of the section called “Combining Etag
and Last-Modified”
would then look like this if using public:
true
:def index @companies = Company.order(:id) fresh_when :etag => @companies.all, :last_modified => @companies.maximum(:updated_at), :public => true end def show @company = Company.find(params[:id]) fresh_when @company, public: true end
$ curl -I http://0.0.0.0:3000/companies HTTP/1.1 200 OK Etag: "d45a37972109e8ccea1160d81a6ff79d" Last-Modified: Sat, 14 Jul 2012 12:40:25 GMT Content-Type: text/html; charset=utf-8 Cache-Control: public [...]
Cache-Control: public
tells all proxies
that they can also cache this web page.csrf_meta_tag
in the default
app/views/layouts/application.html.erb
layout
dependent on the question whether the page may be cached publicly or
not:<%= csrf_meta_tag unless response.cache_control[:public] %>
Etag
and Last-Modified
we
assume in the section called “Etag” and the section called “Last-Modified” that the web browser definitely
checks once more with the web server if the cached version of a web page
is still current. This is a very safe approach.Etag
and
Last-Modified
examples: Cache-Control: max-age=0, private, must-revalidateThe item
must-revalidate
tells the web browser that it should
definitely check back with the web server to see if a web page has
changed in the meantime. The second parameter private
means
that only the web browser is allowed to cache this page. Any proxies on
the way are not permitted to cache this page.expires_in
. The controller
app/controllers/companies.rb
would then contain the
following code for the method index
and
show
:def index @companies = Company.order(:id) expires_in 2.minutes fresh_when :etag => @companies.all, :last_modified => @companies.maximum(:updated_at) end def show @company = Company.find(params[:id]) expires_in 2.minutes fresh_when @company end
$ curl -I http://0.0.0.0:3000/companies HTTP/1.1 200 OK Etag: "d45a37972109e8ccea1160d81a6ff79d" Last-Modified: Sat, 14 Jul 2012 12:40:25 GMT Content-Type: text/html; charset=utf-8 Cache-Control: max-age=120, private [...]
max-age=120
) and we no longer need
must-revalidate
. So in the next 120 seconds, the web
browser does not need to check back with the web server to see if the
content of this page has changed.location ^~ /assets/ { gzip_static on; expires max; add_header Cache-Control public; }
Updates about this book will be published on my Twitter feed.