Module: Controllers::CloudflareCaching

Extended by:
ActiveSupport::Concern
Included in:
ApplicationController
Defined in:
app/concerns/controllers/cloudflare_caching.rb

Overview

Cloudflare CDN caching utilities for edge caching

Usage:

1. Declare which actions are edge-cached (prevents ghost guest creation):

edge_cached # all actions
edge_cached only: %i[index show] # specific actions

2. In the controller action, set cache params:

set_cloudflare_cache(time_in_secs: 1.week.to_i, tags: ['product-123'])

To bust cache on error:

reset_cloudflare_cache

Why edge_cached matters:
Edge-cached pages skip the session cookie (Set-Cookie header) so Cloudflare
can cache them. Without edge_cached, init_current_user creates a guest user
in the database, then skip_session discards the session -- orphaning the guest.
This causes "ghost guest" proliferation.

See: https://blog.cloudflare.com/cdn-cache-control/

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.edge_cached(only: nil) ⇒ Object

Declare actions as edge-cacheable. This automatically skips init_current_user
to prevent ghost guest creation on pages served from Cloudflare's CDN.

Examples:

Cache all actions in the controller

edge_cached

Cache specific actions only

edge_cached only: %i[index show]


44
45
46
47
48
49
50
51
52
# File 'app/concerns/controllers/cloudflare_caching.rb', line 44

def edge_cached(only: nil)
  if only
    skip_before_action :init_current_user, only: only
    self._edge_cached_actions = Array(only).to_set(&:to_s).freeze
  else
    skip_before_action :init_current_user
    self._edge_cached_actions = :all
  end
end

Instance Method Details

#edge_cached_action?Boolean

Returns true if the current action was declared via edge_cached

Returns:

  • (Boolean)


56
57
58
59
60
61
62
# File 'app/concerns/controllers/cloudflare_caching.rb', line 56

def edge_cached_action?
  config = self.class._edge_cached_actions
  return false if config.nil?
  return true if config == :all

  config.include?(action_name)
end

#reset_cloudflare_cacheObject



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/concerns/controllers/cloudflare_caching.rb', line 116

def reset_cloudflare_cache
  return unless response

  # Remove all Cloudflare-specific cache headers
  response.delete_header('Cloudflare-CDN-Cache-Control')
  response.delete_header('Cache-Tag')

  # Explicitly tell Cloudflare to NOT cache this response
  # This prevents edge caching of error pages even if other headers suggest caching
  response.set_header('Cloudflare-CDN-Cache-Control', 'no-store')

  # Also prevent browser caching
  expires_now

  @edge_cached = false
end

#set_cloudflare_cache(time_in_secs: 3.days.to_i, stale_time_in_secs: 1.day.to_i, stale_while_revalidate_in_secs: 1.day.to_i, public_cache_expires_in: 3.days, tags: nil) ⇒ Object

Applies headers for caching specifically for cloudflare's edge

Parameters:

  • time_in_secs (Integer) (defaults to: 3.days.to_i)

    Normal time to keep a public response in cache

  • stale_time_in_secs (Integer) (defaults to: 1.day.to_i)

    Time to keep serving stale on origin error (stale-if-error)

  • stale_while_revalidate_in_secs (Integer) (defaults to: 1.day.to_i)

    Time the edge may serve stale
    while refetching in the background (stale-while-revalidate). Eliminates the
    "first hit after expiry pays the origin round-trip" penalty.

  • public_cache_expires_in (ActiveSupport::Duration) (defaults to: 3.days)

    Browser cache expiry

  • tags (Array<String>) (defaults to: nil)

    Cache tags for selective purging



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'app/concerns/controllers/cloudflare_caching.rb', line 72

def set_cloudflare_cache(time_in_secs: 3.days.to_i, stale_time_in_secs: 1.day.to_i,
                         stale_while_revalidate_in_secs: 1.day.to_i,
                         public_cache_expires_in: 3.days, tags: nil)
  return unless response&.ok?
  # Render-time opt-out: any helper that produced per-user content during
  # this request (e.g. an inline lead form prefilled with @context_user
  # data) sets @_skip_edge_cache so we don't cache the personalized HTML
  # for everyone.
  return if @_skip_edge_cache

  warn_if_not_edge_cached

  expires_in public_cache_expires_in, public: true
  skip_session # Necessary otherwise you will not get the benefit of this method
  cdn_cache_controls = []
  cdn_cache_controls << "max-age=#{time_in_secs}"
  cdn_cache_controls << "stale-if-error=#{stale_time_in_secs}"
  cdn_cache_controls << "stale-while-revalidate=#{stale_while_revalidate_in_secs}" if stale_while_revalidate_in_secs&.positive?
  response.set_header('Cloudflare-CDN-Cache-Control', cdn_cache_controls.join(','))
  response.set_header('Cache-Tag', tags.join(',')) if tags.present?
  # Stamp the asset-manifest version onto THIS cacheable body so the www-edge
  # worker's ETag/304 + X-Asset-Version reflect the bundles this HTML references,
  # not the live manifest. It rides with the cached body, so a 3-day-stale edge
  # page carries its own (older) version and the worker stops 304-ing browsers
  # onto HTML that points at newer bundles. See www-edge-worker.js
  # handleAssetVersioning.
  response.set_header('X-Asset-Version', WebpackManifestLoader.asset_version)
  @edge_cached = true
end

#skip_edge_cache!Object

Render-time signal that this response is per-user and must not be edge
cached. Safe to call multiple times; takes precedence over any later
set_cloudflare_cache call (typically run from an after_action).

Used by inline lead-form helpers so any page that embeds a personalized
form is automatically excluded from Cloudflare caching, regardless of
which controller renders it. The "lazy modal" lead-form pattern
(lead_form_modal / lazy_lead_form_modal) is unaffected — its form
is fetched per-request via Turbo Frame from an uncached endpoint, so
the host page can stay cached.



112
113
114
# File 'app/concerns/controllers/cloudflare_caching.rb', line 112

def skip_edge_cache!
  @_skip_edge_cache = true
end

#skip_sessionObject

For some static content, we don't need to track the session
This helps with picky CDNs throwing off the cache if a cookie is present



135
136
137
# File 'app/concerns/controllers/cloudflare_caching.rb', line 135

def skip_session
  request.session_options[:skip] = true
end