Skip to content

Commit 0da7aaf

Browse files
authored
Merge pull request rails#55033 from egg528/add-http-request-cache-control-directives-support
Add support for Cache-Control request directives
2 parents fb89f44 + 55b54a3 commit 0da7aaf

File tree

3 files changed

+249
-0
lines changed

3 files changed

+249
-0
lines changed

actionpack/CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1+
* Add comprehensive support for HTTP Cache-Control request directives according to RFC 9111.
2+
3+
Provides a `request.cache_control_directives` object that gives access to request cache directives:
4+
5+
```ruby
6+
# Boolean directives
7+
request.cache_control_directives.only_if_cached? # => true/false
8+
request.cache_control_directives.no_cache? # => true/false
9+
request.cache_control_directives.no_store? # => true/false
10+
request.cache_control_directives.no_transform? # => true/false
11+
12+
# Value directives
13+
request.cache_control_directives.max_age # => integer or nil
14+
request.cache_control_directives.max_stale # => integer or nil (or true for valueless max-stale)
15+
request.cache_control_directives.min_fresh # => integer or nil
16+
request.cache_control_directives.stale_if_error # => integer or nil
17+
18+
# Special helpers for max-stale
19+
request.cache_control_directives.max_stale? # => true if max-stale present (with or without value)
20+
request.cache_control_directives.max_stale_unlimited? # => true only for valueless max-stale
21+
```
22+
23+
Example usage:
24+
25+
```ruby
26+
def show
27+
if request.cache_control_directives.only_if_cached?
28+
@article = Article.find_cached(params[:id])
29+
return head(:gateway_timeout) if @article.nil?
30+
else
31+
@article = Article.find(params[:id])
32+
end
33+
34+
render :show
35+
end
36+
```
37+
38+
*egg528*
39+
140
* Add assert_in_body/assert_not_in_body as the simplest way to check if a piece of text is in the response body.
241

342
*DHH*

actionpack/lib/action_dispatch/http/cache.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,114 @@ def fresh?(response)
6363
success
6464
end
6565
end
66+
67+
def cache_control_directives
68+
@cache_control_directives ||= CacheControlDirectives.new(get_header("HTTP_CACHE_CONTROL"))
69+
end
70+
71+
# Represents the HTTP Cache-Control header for requests,
72+
# providing methods to access various cache control directives
73+
# Reference: https://www.rfc-editor.org/rfc/rfc9111.html#name-request-directives
74+
class CacheControlDirectives
75+
def initialize(cache_control_header)
76+
@only_if_cached = false
77+
@no_cache = false
78+
@no_store = false
79+
@no_transform = false
80+
@max_age = nil
81+
@max_stale = nil
82+
@min_fresh = nil
83+
@stale_if_error = false
84+
parse_directives(cache_control_header)
85+
end
86+
87+
# Returns true if the only-if-cached directive is present.
88+
# This directive indicates that the client only wishes to obtain a
89+
# stored response. If a valid stored response is not available,
90+
# the server should respond with a 504 (Gateway Timeout) status.
91+
def only_if_cached?
92+
@only_if_cached
93+
end
94+
95+
# Returns true if the no-cache directive is present.
96+
# This directive indicates that a cache must not use the response
97+
# to satisfy subsequent requests without successful validation on the origin server.
98+
def no_cache?
99+
@no_cache
100+
end
101+
102+
# Returns true if the no-store directive is present.
103+
# This directive indicates that a cache must not store any part of the
104+
# request or response.
105+
def no_store?
106+
@no_store
107+
end
108+
109+
# Returns true if the no-transform directive is present.
110+
# This directive indicates that a cache or proxy must not transform the payload.
111+
def no_transform?
112+
@no_transform
113+
end
114+
115+
# Returns the value of the max-age directive.
116+
# This directive indicates that the client is willing to accept a response
117+
# whose age is no greater than the specified number of seconds.
118+
attr_reader :max_age
119+
120+
# Returns the value of the max-stale directive.
121+
# When max-stale is present with a value, returns that integer value.
122+
# When max-stale is present without a value, returns true (unlimited staleness).
123+
# When max-stale is not present, returns nil.
124+
attr_reader :max_stale
125+
126+
# Returns true if max-stale directive is present (with or without a value)
127+
def max_stale?
128+
!@max_stale.nil?
129+
end
130+
131+
# Returns true if max-stale directive is present without a value (unlimited staleness)
132+
def max_stale_unlimited?
133+
@max_stale == true
134+
end
135+
136+
# Returns the value of the min-fresh directive.
137+
# This directive indicates that the client is willing to accept a response
138+
# whose freshness lifetime is no less than its current age plus the specified time in seconds.
139+
attr_reader :min_fresh
140+
141+
# Returns the value of the stale-if-error directive.
142+
# This directive indicates that the client is willing to accept a stale response
143+
# if the check for a fresh one fails with an error for the specified number of seconds.
144+
attr_reader :stale_if_error
145+
146+
private
147+
def parse_directives(header_value)
148+
return unless header_value
149+
150+
header_value.delete(" ").downcase.split(",").each do |directive|
151+
name, value = directive.split("=", 2)
152+
153+
case name
154+
when "max-age"
155+
@max_age = value.to_i
156+
when "min-fresh"
157+
@min_fresh = value.to_i
158+
when "stale-if-error"
159+
@stale_if_error = value.to_i
160+
when "no-cache"
161+
@no_cache = true
162+
when "no-store"
163+
@no_store = true
164+
when "no-transform"
165+
@no_transform = true
166+
when "only-if-cached"
167+
@only_if_cached = true
168+
when "max-stale"
169+
@max_stale = value ? value.to_i : true
170+
end
171+
end
172+
end
173+
end
66174
end
67175

68176
module Response

actionpack/test/dispatch/request_test.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,3 +1446,105 @@ def setup
14461446
assert_instance_of(ActionDispatch::Request::Session::Options, ActionDispatch::Request::Session::Options.find(@request))
14471447
end
14481448
end
1449+
1450+
class RequestCacheControlDirectives < BaseRequestTest
1451+
test "lazily initializes cache_control_directives" do
1452+
request = stub_request
1453+
assert_not_includes request.instance_variables, :@cache_control_directives
1454+
1455+
request.cache_control_directives
1456+
assert_includes request.instance_variables, :@cache_control_directives
1457+
end
1458+
1459+
test "only_if_cached? is true when only-if-cached is the sole directive" do
1460+
request = stub_request("HTTP_CACHE_CONTROL" => "only-if-cached")
1461+
assert_predicate request.cache_control_directives, :only_if_cached?
1462+
end
1463+
1464+
test "only_if_cached? is true when only-if-cached appears among multiple directives" do
1465+
request = stub_request("HTTP_CACHE_CONTROL" => "max-age=60, only-if-cached")
1466+
assert_predicate request.cache_control_directives, :only_if_cached?
1467+
end
1468+
1469+
test "only_if_cached? is false when Cache-Control header is missing" do
1470+
request = stub_request
1471+
assert_not_predicate request.cache_control_directives, :only_if_cached?
1472+
end
1473+
1474+
test "no_cache? properly detects the no-cache directive" do
1475+
request = stub_request("HTTP_CACHE_CONTROL" => "no-cache")
1476+
assert_predicate request.cache_control_directives, :no_cache?
1477+
end
1478+
1479+
test "no_store? properly detects the no-store directive" do
1480+
request = stub_request("HTTP_CACHE_CONTROL" => "no-store")
1481+
assert_predicate request.cache_control_directives, :no_store?
1482+
end
1483+
1484+
test "no_transform? properly detects the no-transform directive" do
1485+
request = stub_request("HTTP_CACHE_CONTROL" => "no-transform")
1486+
assert_predicate request.cache_control_directives, :no_transform?
1487+
end
1488+
1489+
test "max_age properly returns the max-age directive value" do
1490+
request = stub_request("HTTP_CACHE_CONTROL" => "max-age=60")
1491+
assert_equal 60, request.cache_control_directives.max_age
1492+
end
1493+
1494+
test "max_stale properly returns the max-stale directive value" do
1495+
request = stub_request("HTTP_CACHE_CONTROL" => "max-stale=300")
1496+
assert_equal 300, request.cache_control_directives.max_stale
1497+
end
1498+
1499+
test "max_stale returns true when max-stale is present without a value" do
1500+
request = stub_request("HTTP_CACHE_CONTROL" => "max-stale")
1501+
assert_equal true, request.cache_control_directives.max_stale
1502+
end
1503+
1504+
test "max_stale? returns true when max-stale is present with or without a value" do
1505+
request = stub_request("HTTP_CACHE_CONTROL" => "max-stale=300")
1506+
assert_predicate request.cache_control_directives, :max_stale?
1507+
1508+
request = stub_request("HTTP_CACHE_CONTROL" => "max-stale")
1509+
assert_predicate request.cache_control_directives, :max_stale?
1510+
end
1511+
1512+
test "max_stale? returns false when max-stale is not present" do
1513+
request = stub_request
1514+
assert_not_predicate request.cache_control_directives, :max_stale?
1515+
end
1516+
1517+
test "max_stale_unlimited? returns true only when max-stale is present without a value" do
1518+
request = stub_request("HTTP_CACHE_CONTROL" => "max-stale")
1519+
assert_predicate request.cache_control_directives, :max_stale_unlimited?
1520+
1521+
request = stub_request("HTTP_CACHE_CONTROL" => "max-stale=300")
1522+
assert_not_predicate request.cache_control_directives, :max_stale_unlimited?
1523+
1524+
request = stub_request
1525+
assert_not_predicate request.cache_control_directives, :max_stale_unlimited?
1526+
end
1527+
1528+
test "min_fresh properly returns the min-fresh directive value" do
1529+
request = stub_request("HTTP_CACHE_CONTROL" => "min-fresh=120")
1530+
assert_equal 120, request.cache_control_directives.min_fresh
1531+
end
1532+
1533+
test "stale_if_error properly returns the stale-if-error directive value" do
1534+
request = stub_request("HTTP_CACHE_CONTROL" => "stale-if-error=600")
1535+
assert_equal 600, request.cache_control_directives.stale_if_error
1536+
end
1537+
1538+
test "handles Cache-Control header with whitespace and case insensitivity" do
1539+
request = stub_request("HTTP_CACHE_CONTROL" => " Max-Age=60 , No-Cache ")
1540+
assert_equal 60, request.cache_control_directives.max_age
1541+
assert_predicate request.cache_control_directives, :no_cache?
1542+
end
1543+
1544+
test "ignores unrecognized directives" do
1545+
request = stub_request("HTTP_CACHE_CONTROL" => "max-age=60, unknown-directive, foo=bar")
1546+
assert_equal 60, request.cache_control_directives.max_age
1547+
assert_not_predicate request.cache_control_directives, :no_cache?
1548+
assert_not_predicate request.cache_control_directives, :no_store?
1549+
end
1550+
end

0 commit comments

Comments
 (0)