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.