Skip to content

Commit 1c094d7

Browse files
authored
Merge pull request rails#54833 from heka1024/add-must-understand
Add `must-understand` directive
2 parents 2318163 + 245234d commit 1c094d7

File tree

4 files changed

+93
-1
lines changed

4 files changed

+93
-1
lines changed

actionpack/CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
* Implement `must-understand` directive according to RFC 9111.
2+
3+
The `must-understand` directive indicates that a cache must understand the semantics of the response status code, or discard the response. This directive is enforced to be used only with `no-store` to ensure proper cache behavior.
4+
5+
```ruby
6+
class ArticlesController < ApplicationController
7+
def show
8+
@article = Article.find(params[:id])
9+
10+
if @article.special_format?
11+
must_understand
12+
render status: 203 # Non-Authoritative Information
13+
else
14+
fresh_when @article
15+
end
16+
end
17+
end
18+
```
19+
20+
*heka1024*
21+
122
* The JSON renderer doesn't escape HTML entities or Unicode line separators anymore.
223
324
Using `render json:` will no longer escape `<`, `>`, `&`, `U+2028` and `U+2029` characters that can cause errors

actionpack/lib/action_controller/metal/conditional_get.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,31 @@ def no_store
332332
response.cache_control.replace(no_store: true)
333333
end
334334

335+
# Adds the `must-understand` directive to the `Cache-Control` header, which indicates
336+
# that a cache MUST understand the semantics of the response status code that has been
337+
# received, or discard the response.
338+
#
339+
# This is particularly useful when returning responses with new or uncommon
340+
# status codes that might not be properly interpreted by older caches.
341+
#
342+
# #### Example
343+
#
344+
# def show
345+
# @article = Article.find(params[:id])
346+
#
347+
# if @article.early_access?
348+
# must_understand
349+
# render status: 203 # Non-Authoritative Information
350+
# else
351+
# fresh_when @article
352+
# end
353+
# end
354+
#
355+
def must_understand
356+
response.cache_control[:must_understand] = true
357+
response.cache_control[:no_store] = true
358+
end
359+
335360
private
336361
def combine_etags(validator, options)
337362
[validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact

actionpack/lib/action_dispatch/http/cache.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def strong_etag?
142142
private
143143
DATE = "Date"
144144
LAST_MODIFIED = "Last-Modified"
145-
SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
145+
SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate must-understand])
146146

147147
def generate_weak_etag(validators)
148148
"W/#{generate_strong_etag(validators)}"
@@ -187,6 +187,7 @@ def prepare_cache_control!
187187
PRIVATE = "private"
188188
MUST_REVALIDATE = "must-revalidate"
189189
IMMUTABLE = "immutable"
190+
MUST_UNDERSTAND = "must-understand"
190191

191192
def handle_conditional_get!
192193
# Normally default cache control setting is handled by ETag middleware. But, if
@@ -221,6 +222,7 @@ def merge_and_normalize_cache_control!(cache_control)
221222

222223
if control[:no_store]
223224
options << PRIVATE if control[:private]
225+
options << MUST_UNDERSTAND if control[:must_understand]
224226
options << NO_STORE
225227
elsif control[:no_cache]
226228
options << PUBLIC if control[:public]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
5+
class ConditionalGetDirectivesController < ActionController::Base
6+
def must_understand_action
7+
must_understand
8+
render plain: "using must-understand directive"
9+
end
10+
11+
def cache_control_with_must_understand
12+
fresh_when etag: "123", cache_control: { must_understand: true }
13+
render plain: "with must-understand via cache_control" unless performed?
14+
end
15+
16+
def must_understand_without_no_store
17+
response.cache_control[:no_cache] = true
18+
response.cache_control[:must_understand] = true
19+
render plain: "no-cache with must-understand"
20+
end
21+
end
22+
23+
class ConditionalGetDirectivesTest < ActionController::TestCase
24+
tests ConditionalGetDirectivesController
25+
26+
def test_must_understand
27+
get :must_understand_action
28+
assert_response :success
29+
assert_includes @response.headers["Cache-Control"], "must-understand"
30+
end
31+
32+
def test_cache_control_with_must_understand
33+
get :cache_control_with_must_understand
34+
assert_response :success
35+
assert_not_includes @response.headers["Cache-Control"], "must-understand"
36+
end
37+
38+
def test_must_understand_without_no_store
39+
get :must_understand_without_no_store
40+
assert_response :success
41+
assert_not_includes @response.headers["Cache-Control"], "must-understand"
42+
assert_includes @response.headers["Cache-Control"], "no-cache"
43+
end
44+
end

0 commit comments

Comments
 (0)