Actions provide several features to help you make use of HTTP caching.

Cache control

The Cache-Control response header is the main way to control HTTP caching behavior. You can use the #cache_control method on your action’s response object to set this header.

  1. # app/actions/books/index.rb
  2. module Bookshelf
  3. module Actions
  4. module Books
  5. class Index < Bookshelf::Action
  6. def handle(request, response)
  7. # Sets `Cache-Control: public, max-age: 600`
  8. response.cache_control :public, max_age: 600
  9. end
  10. end
  11. end
  12. end
  13. end

The cache_control method accepts one or more of the following Cache-Control directives as its arguments.

  • :public
  • :private
  • :no_cache
  • :no_store
  • :must_validate
  • :proxy_revalidate
  • max_age: N (N here and below should be an integer representing a period in seconds)
  • s_maxage: N
  • min_fresh: N
  • max_stale: N

Expires

The [Expires response header][mdn-expires] sets the date and time after which the response is considered expired. The Expires header is an older standard, and Cache-Control is preferred by modern browsers.

You can use the #expires method on your action’s response object to set this header along with a matching Cache-Control header.

  1. # app/actions/books/index.rb
  2. module Bookshelf
  3. module Actions
  4. module Books
  5. class Index < Bookshelf::Action
  6. def handle(request, response)
  7. # Sets `Expires: Sun, 20 Nov 2022 17:47:02 GMT, Cache-Control: public, max-age=600`
  8. response.expires 60, :public, max_age: 600
  9. end
  10. end
  11. end
  12. end
  13. end

Conditional requests

HTTP conditional requests are an interaction between the server and a client wherein the result of a request can be changed by comparing the related server resource with the value of a validator. These requests can be useful to validate the content of a client’s cache for the given resource.

For example, for a conditional GET request, there is a two-step workflow:

  1. The response to an initial request includes a “validator” in its headers, either the date of last modification for the resource (a Last-Modified response header and a subsequent If-Modified-Since request header), or an string identifying the version of the resource (an ETag response header and a subsequent If-None-Match request header)
  2. On the next request, the client sends its counterpart request header, and if the server determines the client’s cache is fresh, it can return a 304 Not Modified response allowing the client to use its cached resource

For more detail on conditional requests, see the MDN documentation.

You can use the #fresh method on your action’s response object to set the Last-Modified or ETag validator headers, as well as short circuit action execution when these are included in the request.

Last Modified

Use the #fresh response method with a last_modified_at: option to return a Last-Modified response header and validate a conditional request by its If-Modified-Since header.

In the below example:

  1. The first request to the action (or a request with a stale cache) will see the action handle the request in full before returning its response with a Last-Modified header.
  2. A subsequent request with a matching If-Modified-Since header will receive a 304 Not Modified response, and the action will halt before its processing logic.

For more detail on action halting, see the control flow guide.

  1. # app/actions/books/show.rb
  2. module Bookshelf
  3. module Actions
  4. module Show
  5. class Index < Bookshelf::Action
  6. include Deps['repositories.users']
  7. def handle(request, response)
  8. user = users.find(params[:id])
  9. response.fresh last_modified: user.updated_at
  10. # <request processing logic here>
  11. end
  12. end
  13. end
  14. end
  15. end
  16. # Case 1 (missing or non-matching Last-Modified)
  17. # GET /users/23
  18. # => 200, Last-Modified: Tue, 22 Nov 2022 10:04:30 GMT
  19. # Case 2 (matching Last-Modified)
  20. # GET /users/23, If-Modified-Since: Tue, 22 Nov 2022 10:04:30 GMT
  21. # => 304

ETag

Use the #fresh response method with an etag: option to return an ETag response header and validate a conditional request by its If-None-Match header.

In the below example:

  1. The firsts request to the action (or a request with a stale cache) will see the action handle the request in full before returning its response with an ETag header.
  2. A subsequent request with a matching If-None-Match header will receive a 304 Not Modified response, and the action will halt before its processing logic.

For more detail on action halting, see the control flow guide.

  1. # app/actions/books/show.rb
  2. module Bookshelf
  3. module Actions
  4. module Books
  5. class Show < Bookshelf::Action
  6. include Deps["user_repo"]
  7. def handle(request, response)
  8. user = user_repo.find(params[:id])
  9. response.fresh etag: "#{user.id}-#{user.updated_at}"
  10. # ...
  11. end
  12. end
  13. end
  14. end
  15. end
  16. # Case 1 (missing or non-matching If-None-Match)
  17. # GET /users/23
  18. # => 200, ETag: 84e037c89f8d55442366c4492baddeae
  19. # Case 2 (matching If-None-Match)
  20. # GET /users/23, If-None-Match: 84e037c89f8d55442366c4492baddeae
  21. # => 304