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.

  1. # apps/web/controllers/dashboard/index.rb
  2. require 'hanami/action/cache'
  3. module Web
  4. module Controllers
  5. module Dashboard
  6. class Index
  7. include Web::Action
  8. include Hanami::Action::Cache
  9. cache_control :public, max_age: 600
  10. # => Cache-Control: public, max-age: 600
  11. def call(params)
  12. # ...
  13. end
  14. end
  15. end
  16. end
  17. 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.

  1. # apps/web/controllers/dashboard/index.rb
  2. require 'hanami/action/cache'
  3. module Web
  4. module Controllers
  5. module Dashboard
  6. class Index
  7. include Web::Action
  8. include Hanami::Action::Cache
  9. expires 60, :public, max_age: 300
  10. # => Expires: Mon, 18 May 2015 09:19:18 GMT
  11. # Cache-Control: public, max-age: 300
  12. def call(params)
  13. # ...
  14. end
  15. end
  16. end
  17. end
  18. 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.

  1. # apps/web/controllers/users/show.rb
  2. require 'hanami/action/cache'
  3. module Web
  4. module Controllers
  5. module Users
  6. class Show
  7. include Web::Action
  8. include Hanami::Action::Cache
  9. def call(params)
  10. @user = UserRepository.new.find(params[:id])
  11. fresh etag: etag
  12. # ...
  13. end
  14. private
  15. def etag
  16. "#{ @user.id }-#{ @user.updated_at }"
  17. end
  18. end
  19. end
  20. end
  21. end
  22. # Case 1 (missing or non-matching If-None-Match)
  23. # GET /users/23
  24. # => 200, ETag: 84e037c89f8d55442366c4492baddeae
  25. # Case 2 (matching If-None-Match)
  26. # GET /users/23, If-None-Match: 84e037c89f8d55442366c4492baddeae
  27. # => 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.

  1. # apps/web/controllers/users/show.rb
  2. require 'hanami/action/cache'
  3. module Web
  4. module Controllers
  5. module Users
  6. class Show
  7. include Web::Action
  8. include Hanami::Action::Cache
  9. def call(params)
  10. @user = UserRepository.new.find(params[:id])
  11. fresh last_modified: @user.updated_at
  12. # ...
  13. end
  14. end
  15. end
  16. end
  17. end
  18. # Case 1 (missing or non-matching Last-Modified)
  19. # GET /users/23
  20. # => 200, Last-Modified: Mon, 18 May 2015 10:04:30 GMT
  21. # Case 2 (matching Last-Modified)
  22. # GET /users/23, If-Modified-Since: Mon, 18 May 2015 10:04:30 GMT
  23. # => 304