aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack')
-rw-r--r--actionpack/CHANGELOG.md15
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb4
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb18
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb44
-rw-r--r--actionpack/test/controller/integration_test.rb26
-rw-r--r--actionpack/test/controller/render_test.rb24
8 files changed, 103 insertions, 34 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index ca7ae6621b..8bb2c73e52 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,18 @@
+* Add `Vary: Accept` header when using `Accept` header for response
+
+ For some requests like `/users/1`, Rails uses requests' `Accept`
+ header to determine what to return. And if we don't add `Vary`
+ in the response header, browsers might accidentally cache different
+ types of content, which would cause issues: e.g. javascript got displayed
+ instead of html content. This PR fixes these issues by adding `Vary: Accept`
+ in these types of requests. For more detailed problem description, please read:
+
+ https://github.com/rails/rails/pull/36213
+
+ Fixes #25842
+
+ *Stan Lo*
+
* Fix IntegrationTest `follow_redirect!` to follow redirection using the same HTTP verb when following
a 307 redirection.
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 8ba2b25552..280af50a44 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -28,6 +28,7 @@ module AbstractController
else
_set_rendered_content_type rendered_format
end
+ _set_vary_header
self.response_body = rendered_body
end
@@ -109,6 +110,9 @@ module AbstractController
def _set_html_content_type # :nodoc:
end
+ def _set_vary_header # :nodoc:
+ end
+
def _set_rendered_content_type(format) # :nodoc:
end
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index efa5de313c..fd22c4fa64 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -77,6 +77,10 @@ module ActionController
end
end
+ def _set_vary_header
+ self.headers["Vary"] = "Accept" if request.should_apply_vary_header?
+ end
+
# Normalize arguments by catching blocks and setting them on :update.
def _normalize_args(action = nil, options = {}, &blk)
options = super
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index a2cac49082..ac0ff133eb 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -62,13 +62,7 @@ module ActionDispatch
def formats
fetch_header("action_dispatch.request.formats") do |k|
- params_readable = begin
- parameters[:format]
- rescue *RESCUABLE_MIME_FORMAT_ERRORS
- false
- end
-
- v = if params_readable
+ v = if params_readable?
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
@@ -153,9 +147,19 @@ module ActionDispatch
order.include?(Mime::ALL) ? format : nil
end
+ def should_apply_vary_header?
+ !params_readable? && use_accept_header && valid_accept_header
+ end
+
private
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
+ def params_readable? # :doc:
+ parameters[:format]
+ rescue *RESCUABLE_MIME_FORMAT_ERRORS
+ false
+ end
+
def valid_accept_header # :doc:
(xhr? && (accept.present? || content_mime_type)) ||
(accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 4aee7a6f83..9184676801 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -148,7 +148,7 @@ module ActionDispatch
end
def glob?
- !path.spec.grep(Nodes::Star).empty?
+ path.spec.any?(Nodes::Star)
end
def dispatcher?
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 642f155085..9b5a5cf2b0 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -346,28 +346,6 @@ module ActionDispatch
@cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
end
- def handle_options(options) # :nodoc:
- if options[:expires].respond_to?(:from_now)
- options[:expires] = options[:expires].from_now
- end
-
- options[:path] ||= "/"
-
- if options[:domain] == :all || options[:domain] == "all"
- # If there is a provided tld length then we use it otherwise default domain regexp.
- domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
-
- # If host is not ip and matches domain regexp.
- # (ip confirms to domain regexp so we explicitly check for ip)
- options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
- ".#{$&}"
- end
- elsif options[:domain].is_a? Array
- # If host matches one of the supplied domains without a dot in front of it.
- options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
- end
- end
-
# Sets the cookie named +name+. The second argument may be the cookie's
# value or a hash of options as documented above.
def []=(name, options)
@@ -447,6 +425,28 @@ module ActionDispatch
def write_cookie?(cookie)
request.ssl? || !cookie[:secure] || always_write_cookie
end
+
+ def handle_options(options)
+ if options[:expires].respond_to?(:from_now)
+ options[:expires] = options[:expires].from_now
+ end
+
+ options[:path] ||= "/"
+
+ if options[:domain] == :all || options[:domain] == "all"
+ # If there is a provided tld length then we use it otherwise default domain regexp.
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
+
+ # If host is not ip and matches domain regexp.
+ # (ip confirms to domain regexp so we explicitly check for ip)
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
+ ".#{$&}"
+ end
+ elsif options[:domain].is_a? Array
+ # If host matches one of the supplied domains without a dot in front of it.
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
+ end
+ end
end
class AbstractCookieJar # :nodoc:
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index caacd66bd6..cb7c2467ac 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -543,6 +543,32 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
+ def test_setting_vary_header_when_request_is_xhr_with_accept_header
+ with_test_route_set do
+ get "/get", headers: { "Accept" => "application/json" }, xhr: true
+ assert_equal "Accept", response.headers["Vary"]
+ end
+ end
+
+ def test_not_setting_vary_header_when_format_is_provided
+ with_test_route_set do
+ get "/get", params: { format: "json" }
+ assert_nil response.headers["Vary"]
+ end
+ end
+
+ def test_not_setting_vary_header_when_ignore_accept_header_is_set
+ original_ignore_accept_header = ActionDispatch::Request.ignore_accept_header
+ ActionDispatch::Request.ignore_accept_header = true
+
+ with_test_route_set do
+ get "/get", headers: { "Accept" => "application/json" }, xhr: true
+ assert_nil response.headers["Vary"]
+ end
+ ensure
+ ActionDispatch::Request.ignore_accept_header = original_ignore_accept_header
+ end
+
private
def with_default_headers(headers)
original = ActionDispatch::Response.default_headers
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index a2a6c69dd3..dd76824413 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -4,6 +4,11 @@ require "abstract_unit"
require "controller/fake_models"
class TestControllerWithExtraEtags < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "test/with_implicit_template.erb" => "Hello explicitly!",
+ "test/hello_world.erb" => "Hello world!"
+ )]
+
def self.controller_name; "test"; end
def self.controller_path; "test"; end
@@ -37,6 +42,11 @@ class TestControllerWithExtraEtags < ActionController::Base
end
class ImplicitRenderTestController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "implicit_render_test/hello_world.erb" => "Hello world!",
+ "implicit_render_test/empty_action_with_template.html.erb" => "<h1>Empty action rendered this implicitly.</h1>\n"
+ )]
+
def empty_action
end
@@ -46,6 +56,10 @@ end
module Namespaced
class ImplicitRenderTestController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "namespaced/implicit_render_test/hello_world.erb" => "Hello world!"
+ )]
+
def hello_world
fresh_when(etag: "abc")
end
@@ -293,13 +307,15 @@ end
module TemplateModificationHelper
private
def modify_template(name)
- path = File.expand_path("../fixtures/#{name}.erb", __dir__)
- original = File.read(path)
- File.write(path, "#{original} Modified!")
+ hash = @controller.view_paths.first.instance_variable_get(:@hash)
+ key = name + ".erb"
+ original = hash[key]
+ hash[key] = "#{original} Modified!"
ActionView::LookupContext::DetailsKey.clear
yield
ensure
- File.write(path, original)
+ hash[key] = original
+ ActionView::LookupContext::DetailsKey.clear
end
end