Module: ConcernsOnRails::Controllers::Cacheable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/concerns_on_rails/controllers/cacheable.rb
Overview
HTTP conditional GET + declarative Cache-Control ("fresh_when/stale?-lite" for JSON APIs). Two layers:
1. A declarative `Cache-Control`/`Vary` POLICY per action (the macro).
2. Per-action validators (ETag / Last-Modified) with an automatic
`304 Not Modified` short-circuit (the `stale_resource?` helper).
class Api::ArticlesController < ApplicationController
include ConcernsOnRails::Controllers::Cacheable
http_cache_actions :index, :show, max_age: 5.minutes,
visibility: :public, vary: "Accept"
def show
@article = Article.find(params[:id])
return unless stale_resource?(@article) # 304 + halt when client copy is fresh
render json: @article
end
end
The method names are deliberately distinct from Rails'
ActionController::ConditionalGet (fresh_when / stale? / expires_in)
so including this concern in a real controller never shadows them.
Conditional-GET correctness (the value over a hand-rolled version):
* ETag is a WEAK validator `W/"<md5>"` derived from the resource's cache
key — appropriate for serialized representations (semantic, not
byte-for-byte, equivalence). `If-None-Match` is matched with weak
comparison, honours `*`, and accepts a comma-separated list.
* `Last-Modified` is an IMF-fixdate via `Time#httpdate` (NOT ISO 8601 —
the classic bug). `If-Modified-Since` is compared at whole-second
granularity (HTTP dates carry no sub-second part).
* When BOTH `If-None-Match` and `If-Modified-Since` are sent, the ETag
wins and the date is ignored (RFC 7232 §3.3).
* The 304 is only sent for safe requests (GET/HEAD) and still carries the
validators AND the policy headers (Cache-Control rides the 304, like
Deprecatable's headers ride the 410).
Notes:
* `no_store: true` overrides everything (emits the lone `no-store`).
* `Vary` is appended to any existing `Vary` header, de-duplicated.
* No positional actions = catch-all; the LAST matching rule wins (the
Deprecatable convention — caching policy is an override).
* Works on bare objects (every `request`/`response` touch is guarded), so
it is testable without the full Rails stack.
* For write-side preconditions (`If-Match` / `If-Unmodified-Since` → 412)
reach for Rails' own conditional-GET helpers; this concern covers the
read path.
Defined Under Namespace
Modules: ClassMethods
Constant Summary collapse
- LABEL =
"ConcernsOnRails::Controllers::Cacheable".freeze
- VALID_VISIBILITY =
%i[public private].freeze
- SAFE_METHODS =
%w[GET HEAD].freeze
Instance Method Summary collapse
-
#apply_http_cache_headers ⇒ Object
after_action entry point.
-
#apply_http_no_store ⇒ Object
Early half of the policy (see the included block): only no_store, and idempotent with apply_http_cache_headers re-applying it after render.
-
#cache_etag_for(resource) ⇒ Object
Override points for deriving validators from a resource.
- #cache_last_modified_for(resource) ⇒ Object
-
#request_matches_cache?(etag: nil, last_modified: nil) ⇒ Boolean
Side-effect-free: does the request's precondition match these validators?.
-
#set_cache_validators(resource = nil, etag: nil, last_modified: nil) ⇒ Object
Set the ETag/Last-Modified response headers (no short-circuit).
-
#stale_resource?(resource = nil, etag: nil, last_modified: nil) ⇒ Boolean
Set ETag/Last-Modified validators for the resource, then — for a safe request whose precondition matches — send 304 and return false.
Instance Method Details
#apply_http_cache_headers ⇒ Object
after_action entry point. Public so host apps can skip_after_action it
or override it. Emits the matching action's Cache-Control + Vary.
142 143 144 145 146 147 148 149 150 151 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 142 def apply_http_cache_headers rule = http_cache_rule_for_action return unless rule return unless respond_to?(:response) && response value = http_cache_control_value(rule) response.set_header("Cache-Control", value) if value response.set_header("Vary", http_cache_merge_vary(rule[:vary])) if rule[:vary] nil end |
#apply_http_no_store ⇒ Object
Early half of the policy (see the included block): only no_store, and idempotent with apply_http_cache_headers re-applying it after render.
155 156 157 158 159 160 161 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 155 def apply_http_no_store rule = http_cache_rule_for_action return unless rule && rule[:no_store] return unless respond_to?(:response) && response response.set_header("Cache-Control", "no-store") end |
#cache_etag_for(resource) ⇒ Object
Override points for deriving validators from a resource.
204 205 206 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 204 def cache_etag_for(resource) http_cache_weak_etag(http_cache_key_for(resource)) end |
#cache_last_modified_for(resource) ⇒ Object
208 209 210 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 208 def cache_last_modified_for(resource) (resource) end |
#request_matches_cache?(etag: nil, last_modified: nil) ⇒ Boolean
Side-effect-free: does the request's precondition match these validators?
189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 189 def request_matches_cache?(etag: nil, last_modified: nil) if_none_match = http_cache_request_header("If-None-Match") if_modified_since = http_cache_request_header("If-Modified-Since") # If-None-Match takes precedence when present (RFC 7232 §3.3). if if_none_match etag && http_cache_etag_matches?(if_none_match, etag) elsif if_modified_since last_modified ? http_cache_not_modified_since?(if_modified_since, last_modified) : false else false end end |
#set_cache_validators(resource = nil, etag: nil, last_modified: nil) ⇒ Object
Set the ETag/Last-Modified response headers (no short-circuit). Returns the computed { etag:, last_modified: } pair.
181 182 183 184 185 186 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 181 def set_cache_validators(resource = nil, etag: nil, last_modified: nil) etag ||= cache_etag_for(resource) unless resource.nil? last_modified ||= cache_last_modified_for(resource) unless resource.nil? http_cache_write_validators(etag, last_modified) { etag: etag, last_modified: last_modified } end |
#stale_resource?(resource = nil, etag: nil, last_modified: nil) ⇒ Boolean
Set ETag/Last-Modified validators for the resource, then — for a safe
request whose precondition matches — send 304 and return false. Returns
true when the client must be sent a fresh body. Mirrors Rails stale?.
166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 166 def stale_resource?(resource = nil, etag: nil, last_modified: nil) # Unsafe methods get neither a 304 nor validators: a POST response must # not advertise an ETag a client could replay against GET (pre-1.22 the # validators were written before the safe-method check). return true unless http_cache_safe_request? validators = set_cache_validators(resource, etag: etag, last_modified: last_modified) return true unless request_matches_cache?(etag: validators[:etag], last_modified: validators[:last_modified]) http_cache_send_not_modified false end |