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.
-
#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.
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 135 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 |
#cache_etag_for(resource) ⇒ Object
Override points for deriving validators from a resource.
183 184 185 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 183 def cache_etag_for(resource) http_cache_weak_etag(http_cache_key_for(resource)) end |
#cache_last_modified_for(resource) ⇒ Object
187 188 189 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 187 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?
168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 168 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.
160 161 162 163 164 165 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 160 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?`.
149 150 151 152 153 154 155 156 |
# File 'lib/concerns_on_rails/controllers/cacheable.rb', line 149 def stale_resource?(resource = nil, etag: nil, last_modified: nil) validators = set_cache_validators(resource, etag: etag, last_modified: last_modified) return true unless http_cache_safe_request? return true unless request_matches_cache?(etag: validators[:etag], last_modified: validators[:last_modified]) http_cache_send_not_modified false end |