We refer to HTTP caching as the set of techniques for HTTP/1.1 and implemented by browser vendors in order to make faster interactions with the server. There are a few headers that, if sent, will enable these HTTP caching mechanisms.
Cache Control
Actions offer a DSL to set a special header Cache-Control
. The first argument is a cache response directive like :public
or "must-revalidate"
, while the second argument is a set of options like :max_age
.
# apps/web/controllers/dashboard/index.rb
require 'hanami/action/cache'
module Web
module Controllers
module Dashboard
class Index
include Web::Action
include Hanami::Action::Cache
cache_control :public, max_age: 600
# => Cache-Control: public, max-age: 600
def call(params)
# ...
end
end
end
end
end
Expires
Another HTTP caching special header is Expires
. It can be used for retrocompatibility with old browsers which don’t understand Cache-Control
.
Hanami’s solution for expire combines support for all the browsers by sending both the headers.
# apps/web/controllers/dashboard/index.rb
require 'hanami/action/cache'
module Web
module Controllers
module Dashboard
class Index
include Web::Action
include Hanami::Action::Cache
expires 60, :public, max_age: 300
# => Expires: Mon, 18 May 2015 09:19:18 GMT
# Cache-Control: public, max-age: 300
def call(params)
# ...
end
end
end
end
end
Conditional GET
Conditional GET is a two step workflow to inform browsers that a resource hasn’t changed since the last visit. At the end of the first request, the response includes special HTTP response headers that the browser will use next time it comes back. If the header matches the value that the server calculates, then the resource is still cached and a 304
status (Not Modified) is returned.
ETag
The first way to match a resource freshness is to use an identifier (usually an MD5 token). Let’s specify it with fresh etag:
.
If the given identifier does NOT match the If-None-Match
request header, the request will return a 200
with an ETag
response header with that value. If the header does match, the action will be halted and a 304
will be returned.
# apps/web/controllers/users/show.rb
require 'hanami/action/cache'
module Web
module Controllers
module Users
class Show
include Web::Action
include Hanami::Action::Cache
def call(params)
@user = UserRepository.new.find(params[:id])
fresh etag: etag
# ...
end
private
def etag
"#{ @user.id }-#{ @user.updated_at }"
end
end
end
end
end
# Case 1 (missing or non-matching If-None-Match)
# GET /users/23
# => 200, ETag: 84e037c89f8d55442366c4492baddeae
# Case 2 (matching If-None-Match)
# GET /users/23, If-None-Match: 84e037c89f8d55442366c4492baddeae
# => 304
Last Modified
The second way is to use a timestamp via fresh last_modified:
.
If the given timestamp does NOT match If-Modified-Since
request header, it will return a 200
and set the Last-Modified
response header with the timestamp value. If the timestamp does match, the action will be halted and a 304
will be returned.
# apps/web/controllers/users/show.rb
require 'hanami/action/cache'
module Web
module Controllers
module Users
class Show
include Web::Action
include Hanami::Action::Cache
def call(params)
@user = UserRepository.new.find(params[:id])
fresh last_modified: @user.updated_at
# ...
end
end
end
end
end
# Case 1 (missing or non-matching Last-Modified)
# GET /users/23
# => 200, Last-Modified: Mon, 18 May 2015 10:04:30 GMT
# Case 2 (matching Last-Modified)
# GET /users/23, If-Modified-Since: Mon, 18 May 2015 10:04:30 GMT
# => 304