All Articles

Rails 7 adds caching? and uncachable! helper

We often hear Cache invalidation is a hard problem in computer science and could cause bugs but sometimes, caching something that should not be cached is a potential source of bugs and security vulnerabilities. Rails has a built-in mechanism for Fragment Caching, which stores part of the rendered view as a fragment. For subsequent requests, the pre-saved fragment is used instead of rendering it again.

But this could cause some serious bugs! For example, we could have an S3 URL helper which generates a unique presigned URL for each product or one could write a form helper that outputs a request-specific auth token. In such cases, it is better to avoid fragment caching.

Before

We can implement fragment caching using the cache helper.

views/products/index.html.erb

  <table>
    <thead>
        <tr>
        <th>Title</th>
        <th>Description</th>
        <th>Image</th>
        </tr>
    </thead>

    <tbody>
        <% @products.each do |product| %>
          <% cache(product) do %>
              <%= render product %>
          <% end %>
        <% end %>
    </tbody>
  </table>

views/products/_product.html.erb

  <tr>
    <td><%= product.title %></td>
    <td><%= product.description %></td>
    <td><%= image_tag(generate_presigned_url(product.image_url)) %></td>
  </tr>

But there is a bug because we get a cached version of the product each time we render despite generating a unique presigned URL each time. To resolve this, we need to include cacheable in the Product partial. If someone tries to cache the product partial, it will throw an ActionView::Template::Error error.

After

  <tr>
    <%= uncacheable! %>
    <td><%= product.title %></td>
    <td><%= product.description %></td>
    <td><%= image_tag(generate_presigned_url(product.image_url)) %></td>
  </tr>

which would result in,

  ActionView::Template::Error (can't be fragment cached):
    1: <tr>
    2:     <%= uncacheable! %>
    3:   <td><%= product.title %></td>
    4:   <td><%= product.description %></td>
    5:   <td><%= image_tag(generate_presigned_url(product.image_url)) %></td>

  app/views/products/_product.html.erb:2

We can also use the caching? helper to check whether the current code path is being cached or to enforce caching.

  <tr>
    <%= raise Exception.new "This partial needs to be cached" unless caching? %>
    <td><%= product.title %></td>
    <td><%= product.description %></td>
  </tr>

For more discussion related to this change, please refer to this PR.