aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch')
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb50
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb15
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb36
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb117
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb125
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb11
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb159
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb19
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb12
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb14
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb38
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb14
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb128
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb6
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb46
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing.rb3
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb20
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb87
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb3
28 files changed, 490 insertions, 447 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index cc1cb3f0f0..1d0a6b6eb3 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -1,4 +1,3 @@
-
module ActionDispatch
module Http
module Cache
@@ -8,13 +7,13 @@ module ActionDispatch
HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
def if_modified_since
- if since = env[HTTP_IF_MODIFIED_SINCE]
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
Time.rfc2822(since) rescue nil
end
end
def if_none_match
- env[HTTP_IF_NONE_MATCH]
+ get_header HTTP_IF_NONE_MATCH
end
def if_none_match_etags
@@ -51,42 +50,46 @@ module ActionDispatch
end
module Response
- attr_reader :cache_control, :etag
- alias :etag? :etag
+ attr_reader :cache_control
def last_modified
- if last = headers[LAST_MODIFIED]
+ if last = get_header(LAST_MODIFIED)
Time.httpdate(last)
end
end
def last_modified?
- headers.include?(LAST_MODIFIED)
+ have_header? LAST_MODIFIED
end
def last_modified=(utc_time)
- headers[LAST_MODIFIED] = utc_time.httpdate
+ set_header LAST_MODIFIED, utc_time.httpdate
end
def date
- if date_header = headers[DATE]
+ if date_header = get_header(DATE)
Time.httpdate(date_header)
end
end
def date?
- headers.include?(DATE)
+ have_header? DATE
end
def date=(utc_time)
- headers[DATE] = utc_time.httpdate
+ set_header DATE, utc_time.httpdate
end
def etag=(etag)
key = ActiveSupport::Cache.expand_cache_key(etag)
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
+ set_header ETAG, %("#{Digest::MD5.hexdigest(key)}")
end
+ def etag
+ get_header ETAG
+ end
+ alias :etag? :etag
+
private
DATE = 'Date'.freeze
@@ -96,7 +99,7 @@ module ActionDispatch
SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
def cache_control_segments
- if cache_control = self[CACHE_CONTROL]
+ if cache_control = get_header(CACHE_CONTROL)
cache_control.delete(' ').split(',')
else
[]
@@ -123,12 +126,11 @@ module ActionDispatch
def prepare_cache_control!
@cache_control = cache_control_headers
- @etag = self[ETAG]
end
def handle_conditional_get!
if etag? || last_modified? || !@cache_control.empty?
- set_conditional_cache_control!
+ set_conditional_cache_control!(@cache_control)
end
end
@@ -138,24 +140,24 @@ module ActionDispatch
PRIVATE = "private".freeze
MUST_REVALIDATE = "must-revalidate".freeze
- def set_conditional_cache_control!
+ def set_conditional_cache_control!(cache_control)
control = {}
cc_headers = cache_control_headers
if extras = cc_headers.delete(:extras)
- @cache_control[:extras] ||= []
- @cache_control[:extras] += extras
- @cache_control[:extras].uniq!
+ cache_control[:extras] ||= []
+ cache_control[:extras] += extras
+ cache_control[:extras].uniq!
end
control.merge! cc_headers
- control.merge! @cache_control
+ control.merge! cache_control
if control.empty?
- self[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
+ set_header CACHE_CONTROL, DEFAULT_CACHE_CONTROL
elsif control[:no_cache]
- self[CACHE_CONTROL] = NO_CACHE
+ set_header CACHE_CONTROL, NO_CACHE
if control[:extras]
- self[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
+ set_header(CACHE_CONTROL, get_header(CACHE_CONTROL) + ", #{control[:extras].join(', ')}")
end
else
extras = control[:extras]
@@ -167,7 +169,7 @@ module ActionDispatch
options << MUST_REVALIDATE if control[:must_revalidate]
options.concat(extras) if extras
- self[CACHE_CONTROL] = options.join(", ")
+ set_header CACHE_CONTROL, options.join(", ")
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index e70e90018c..9c0f39f2e7 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/object/duplicable'
require 'action_dispatch/http/parameter_filter'
module ActionDispatch
@@ -25,7 +23,7 @@ module ActionDispatch
NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
- def initialize(env)
+ def initialize
super
@filtered_parameters = nil
@filtered_env = nil
@@ -50,13 +48,13 @@ module ActionDispatch
protected
def parameter_filter
- parameter_filter_for get_header("action_dispatch.parameter_filter") {
+ parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
def env_filter
- user_key = get_header("action_dispatch.parameter_filter") {
+ user_key = fetch_header("action_dispatch.parameter_filter") {
return NULL_ENV_FILTER
}
parameter_filter_for(Array(user_key) + ENV_MATCH)
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
index 94c1f2b41f..f4b806b8b5 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -16,7 +16,7 @@ module ActionDispatch
def location_filters
if request
- request.env['action_dispatch.redirect_filter'] || []
+ request.get_header('action_dispatch.redirect_filter') || []
else
[]
end
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index fbdec6c132..9a3aaca3f0 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -64,7 +64,7 @@ module ActionDispatch
# If the code block is provided, then it will be run and
# its result returned.
def fetch(key, default = DEFAULT)
- @req.get_header(env_name(key)) do
+ @req.fetch_header(env_name(key)) do
return default unless default == DEFAULT
return yield if block_given?
raise NameError, key
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 9e14eea721..cab60a508a 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -15,12 +15,13 @@ module ActionDispatch
# For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_mime_type
- get_header("action_dispatch.request.content_type") do
- if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/
+ fetch_header("action_dispatch.request.content_type") do |k|
+ v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
+ set_header k, v
end
end
@@ -30,14 +31,15 @@ module ActionDispatch
# Returns the accepted MIME type for the request.
def accepts
- get_header("action_dispatch.request.accepts") do
+ fetch_header("action_dispatch.request.accepts") do |k|
header = get_header('HTTP_ACCEPT').to_s.strip
- if header.empty?
+ v = if header.empty?
[content_mime_type]
else
Mime::Type.parse(header)
end
+ set_header k, v
end
end
@@ -52,14 +54,14 @@ module ActionDispatch
end
def formats
- get_header("action_dispatch.request.formats") do
+ fetch_header("action_dispatch.request.formats") do |k|
params_readable = begin
parameters[:format]
rescue ActionController::BadRequest
false
end
- if params_readable
+ v = if params_readable
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
@@ -68,6 +70,7 @@ module ActionDispatch
else
[Mime::HTML]
end
+ set_header k, v
end
end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index 4defb7f858..3c9f8cd9e4 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,6 +1,3 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/indifferent_access'
-
module ActionDispatch
module Http
module Parameters
@@ -8,20 +5,23 @@ module ActionDispatch
# Returns both GET and POST \parameters in a single hash.
def parameters
- @env["action_dispatch.request.parameters"] ||= begin
- params = begin
- request_parameters.merge(query_parameters)
- rescue EOFError
- query_parameters.dup
- end
- params.merge!(path_parameters)
- end
+ params = get_header("action_dispatch.request.parameters")
+ return params if params
+
+ params = begin
+ request_parameters.merge(query_parameters)
+ rescue EOFError
+ query_parameters.dup
+ end
+ params.merge!(path_parameters)
+ set_header("action_dispatch.request.parameters", params)
+ params
end
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
- @env.delete('action_dispatch.request.parameters')
- @env[PARAMETERS_KEY] = parameters
+ delete_header('action_dispatch.request.parameters')
+ set_header PARAMETERS_KEY, parameters
end
# Returns a hash with the \parameters used to form the \path of the request.
@@ -29,15 +29,7 @@ module ActionDispatch
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
def path_parameters
- @env[PARAMETERS_KEY] ||= {}
- end
-
- private
-
- # Convert nested Hash to HashWithIndifferentAccess.
- #
- def normalize_encode_params(params)
- ActionDispatch::Request::Utils.normalize_encode_params params
+ get_header(PARAMETERS_KEY) || {}
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 1f480eec73..18504eba6d 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -13,14 +13,14 @@ require 'action_dispatch/http/url'
require 'active_support/core_ext/array/conversions'
module ActionDispatch
- class Request < Rack::Request
+ class Request
+ include Rack::Request::Helpers
include ActionDispatch::Http::Cache::Request
include ActionDispatch::Http::MimeNegotiation
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
-
- HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc:
+ include Rack::Request::Env
autoload :Session, 'action_dispatch/request/session'
autoload :Utils, 'action_dispatch/request/utils'
@@ -31,17 +31,20 @@ module ActionDispatch
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER REMOTE_ADDR
SERVER_NAME SERVER_PROTOCOL
+ ORIGINAL_SCRIPT_NAME
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP
HTTP_X_FORWARDED_FOR HTTP_VERSION
+ HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST
+ SERVER_ADDR
].freeze
ENV_METHODS.each do |env|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset
- @env["#{env}".freeze] # @env["HTTP_ACCEPT_CHARSET".freeze]
+ get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze
end # end
METHOD
end
@@ -67,8 +70,27 @@ module ActionDispatch
end
end
+ PASS_NOT_FOUND = Class.new { # :nodoc:
+ def self.action(_); self; end
+ def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end
+ }
+
+ def controller_class
+ check_path_parameters!
+ params = path_parameters
+
+ if params.key?(:controller)
+ controller_param = params[:controller].underscore
+ params[:action] ||= 'index'
+ const_name = "#{controller_param.camelize}Controller"
+ ActiveSupport::Dependencies.constantize(const_name)
+ else
+ PASS_NOT_FOUND
+ end
+ end
+
def key?(key)
- @env.key?(key)
+ has_header? key
end
# List of HTTP request methods from the following RFCs:
@@ -109,44 +131,44 @@ module ActionDispatch
end
def routes # :nodoc:
- env["action_dispatch.routes".freeze]
+ get_header("action_dispatch.routes".freeze)
end
def routes=(routes) # :nodoc:
- env["action_dispatch.routes".freeze] = routes
- end
-
- def original_script_name # :nodoc:
- env['ORIGINAL_SCRIPT_NAME'.freeze]
+ set_header("action_dispatch.routes".freeze, routes)
end
def engine_script_name(_routes) # :nodoc:
- env[_routes.env_key]
+ get_header(_routes.env_key)
end
def engine_script_name=(name) # :nodoc:
- env[routes.env_key] = name.dup
+ set_header(routes.env_key, name.dup)
end
def request_method=(request_method) #:nodoc:
if check_method(request_method)
- @request_method = env["REQUEST_METHOD"] = request_method
+ @request_method = set_header("REQUEST_METHOD", request_method)
end
end
def controller_instance # :nodoc:
- env['action_controller.instance'.freeze]
+ get_header('action_controller.instance'.freeze)
end
def controller_instance=(controller) # :nodoc:
- env['action_controller.instance'.freeze] = controller
+ set_header('action_controller.instance'.freeze, controller)
+ end
+
+ def http_auth_salt
+ get_header "action_dispatch.http_auth_salt"
end
def show_exceptions? # :nodoc:
# We're treating `nil` as "unset", and we want the default setting to be
# `true`. This logic should be extracted to `env_config` and calculated
# once.
- !(env['action_dispatch.show_exceptions'.freeze] == false)
+ !(get_header('action_dispatch.show_exceptions'.freeze) == false)
end
# Returns a symbol form of the #request_method
@@ -158,7 +180,7 @@ module ActionDispatch
# even if it was overridden by middleware. See #request_method for
# more information.
def method
- @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD'])
+ @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header('REQUEST_METHOD'))
end
# Returns a symbol form of the #method
@@ -181,7 +203,7 @@ module ActionDispatch
# # get '/foo?bar'
# request.original_fullpath # => '/foo?bar'
def original_fullpath
- @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
+ @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath)
end
# Returns the +String+ full path including params of the last URL requested.
@@ -220,7 +242,7 @@ module ActionDispatch
# (case-insensitive), which may need to be manually added depending on the
# choice of JavaScript libraries and frameworks.
def xml_http_request?
- @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
+ get_header('HTTP_X_REQUESTED_WITH') =~ /XMLHttpRequest/i
end
alias :xhr? :xml_http_request?
@@ -232,11 +254,11 @@ module ActionDispatch
# Returns the IP address of client as a +String+,
# usually set by the RemoteIp middleware.
def remote_ip
- @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
+ @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
end
def remote_ip=(remote_ip)
- @env["action_dispatch.remote_ip".freeze] = remote_ip
+ set_header "action_dispatch.remote_ip".freeze, remote_ip
end
ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc:
@@ -248,43 +270,39 @@ module ActionDispatch
# This unique ID is useful for tracing a request from end-to-end as part of logging or debugging.
# This relies on the rack variable set by the ActionDispatch::RequestId middleware.
def request_id
- env[ACTION_DISPATCH_REQUEST_ID]
+ get_header ACTION_DISPATCH_REQUEST_ID
end
def request_id=(id) # :nodoc:
- env[ACTION_DISPATCH_REQUEST_ID] = id
+ set_header ACTION_DISPATCH_REQUEST_ID, id
end
alias_method :uuid, :request_id
- def x_request_id # :nodoc:
- @env[HTTP_X_REQUEST_ID]
- end
-
# Returns the lowercase name of the HTTP server software.
def server_software
- (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
+ (get_header('SERVER_SOFTWARE') && /^([a-zA-Z]+)/ =~ get_header('SERVER_SOFTWARE')) ? $1.downcase : nil
end
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
- unless @env.include? 'RAW_POST_DATA'
+ unless has_header? 'RAW_POST_DATA'
raw_post_body = body
- @env['RAW_POST_DATA'] = raw_post_body.read(content_length)
+ set_header('RAW_POST_DATA', raw_post_body.read(content_length))
raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
end
- @env['RAW_POST_DATA']
+ get_header 'RAW_POST_DATA'
end
# The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO.
def body
- if raw_post = @env['RAW_POST_DATA']
+ if raw_post = get_header('RAW_POST_DATA')
raw_post.force_encoding(Encoding::BINARY)
StringIO.new(raw_post)
else
- @env['rack.input']
+ body_stream
end
end
@@ -295,7 +313,7 @@ module ActionDispatch
end
def body_stream #:nodoc:
- @env['rack.input']
+ get_header('rack.input')
end
# TODO This should be broken apart into AD::Request::Session and probably
@@ -306,20 +324,22 @@ module ActionDispatch
else
self.session = {}
end
- @env['action_dispatch.request.flash_hash'] = nil
+ set_header('action_dispatch.request.flash_hash', nil)
end
def session=(session) #:nodoc:
- Session.set @env, session
+ Session.set self, session
end
def session_options=(options)
- Session::Options.set @env, options
+ Session::Options.set self, options
end
# Override Rack's GET method to support indifferent access
def GET
- @env["action_dispatch.request.query_parameters"] ||= normalize_encode_params(super || {})
+ fetch_header("action_dispatch.request.query_parameters") do |k|
+ set_header k, Request::Utils.normalize_encode_params(super || {})
+ end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:query, e)
end
@@ -327,7 +347,9 @@ module ActionDispatch
# Override Rack's POST method to support indifferent access
def POST
- @env["action_dispatch.request.request_parameters"] ||= normalize_encode_params(super || {})
+ fetch_header("action_dispatch.request.request_parameters") do
+ self.request_parameters = Request::Utils.normalize_encode_params(super || {})
+ end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:request, e)
end
@@ -336,23 +358,24 @@ module ActionDispatch
# Returns the authorization header regardless of whether it was specified directly or through one of the
# proxy alternatives.
def authorization
- @env['HTTP_AUTHORIZATION'] ||
- @env['X-HTTP_AUTHORIZATION'] ||
- @env['X_HTTP_AUTHORIZATION'] ||
- @env['REDIRECT_X_HTTP_AUTHORIZATION']
+ get_header('HTTP_AUTHORIZATION') ||
+ get_header('X-HTTP_AUTHORIZATION') ||
+ get_header('X_HTTP_AUTHORIZATION') ||
+ get_header('REDIRECT_X_HTTP_AUTHORIZATION')
end
- # True if the request came from localhost, 127.0.0.1.
+ # True if the request came from localhost, 127.0.0.1, or ::1.
def local?
LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
end
def request_parameters=(params)
- env["action_dispatch.request.request_parameters".freeze] = params
+ raise if params.nil?
+ set_header("action_dispatch.request.request_parameters".freeze, params)
end
def logger
- env["action_dispatch.logger".freeze]
+ get_header("action_dispatch.logger".freeze)
end
private
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index fd92e89231..d1e1f1fcf6 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -38,8 +38,6 @@ module ActionDispatch # :nodoc:
# The HTTP status code.
attr_reader :status
- attr_writer :sending_file
-
# Get headers for this response.
attr_reader :header
@@ -48,24 +46,10 @@ module ActionDispatch # :nodoc:
delegate :[], :[]=, :to => :@header
delegate :each, :to => :@stream
- # Sets the HTTP response's content MIME type. For example, in the controller
- # you could write this:
- #
- # response.content_type = "text/plain"
- #
- # If a character set has been defined for this response (see charset=) then
- # the character set information will also be included in the content type
- # information.
- attr_reader :content_type
-
- # The charset of the response. HTML wants to know the encoding of the
- # content you're giving them, so we need to send that along.
- attr_reader :charset
-
CONTENT_TYPE = "Content-Type".freeze
SET_COOKIE = "Set-Cookie".freeze
LOCATION = "Location".freeze
- NO_CONTENT_CODES = [204, 304]
+ NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
cattr_accessor(:default_charset) { "utf-8" }
cattr_accessor(:default_headers)
@@ -130,26 +114,22 @@ module ActionDispatch # :nodoc:
self.body, self.status = body, status
- @sending_file = false
@blank = false
@cv = new_cond
@committed = false
@sending = false
@sent = false
- @content_type = nil
- @charset = self.class.default_charset
-
- if content_type = self[CONTENT_TYPE]
- type, charset = content_type.split(/;\s*charset=/)
- @content_type = Mime::Type.lookup(type)
- @charset = charset || self.class.default_charset
- end
prepare_cache_control!
yield self if block_given?
end
+ def have_header?(key); headers.key? key; end
+ def get_header(key); headers[key]; end
+ def set_header(key, v); headers[key] = v; end
+ def delete_header(key); headers.delete key; end
+
def await_commit
synchronize do
@cv.wait_until { @committed }
@@ -194,7 +174,27 @@ module ActionDispatch # :nodoc:
# Sets the HTTP content type.
def content_type=(content_type)
- @content_type = content_type.to_s
+ header_info = parse_content_type
+ set_content_type content_type.to_s, header_info.charset || self.class.default_charset
+ end
+
+ # Sets the HTTP response's content MIME type. For example, in the controller
+ # you could write this:
+ #
+ # response.content_type = "text/plain"
+ #
+ # If a character set has been defined for this response (see charset=) then
+ # the character set information will also be included in the content type
+ # information.
+
+ def content_type
+ parse_content_type.mime_type
+ end
+
+ def sending_file=(v)
+ if true == v
+ self.charset = false
+ end
end
# Sets the HTTP character set. In case of nil parameter
@@ -203,7 +203,20 @@ module ActionDispatch # :nodoc:
# response.charset = 'utf-16' # => 'utf-16'
# response.charset = nil # => 'utf-8'
def charset=(charset)
- @charset = charset.nil? ? self.class.default_charset : charset
+ header_info = parse_content_type
+ if false == charset
+ set_header CONTENT_TYPE, header_info.mime_type
+ else
+ content_type = header_info.mime_type
+ set_content_type content_type, charset || self.class.default_charset
+ end
+ end
+
+ # The charset of the response. HTML wants to know the encoding of the
+ # content you're giving them, so we need to send that along.
+ def charset
+ header_info = parse_content_type
+ header_info.charset || self.class.default_charset
end
# The response code of the request.
@@ -256,25 +269,9 @@ module ActionDispatch # :nodoc:
parts
end
- def set_cookie(key, value)
- ::Rack::Utils.set_cookie_header!(header, key, value)
- end
-
- def delete_cookie(key, value={})
- ::Rack::Utils.delete_cookie_header!(header, key, value)
- end
-
# The location header we'll be responding with.
- def location
- headers[LOCATION]
- end
alias_method :redirect_url, :location
- # Sets the location header we'll be responding with.
- def location=(url)
- headers[LOCATION] = url
- end
-
def close
stream.close if stream.respond_to?(:close)
end
@@ -305,7 +302,7 @@ module ActionDispatch # :nodoc:
# assert_equal 'AuthorOfNewPage', r.cookies['author']
def cookies
cookies = {}
- if header = self[SET_COOKIE]
+ if header = get_header(SET_COOKIE)
header = header.split("\n") if header.respond_to?(:to_str)
header.each do |cookie|
if pair = cookie.split(';').first
@@ -319,6 +316,26 @@ module ActionDispatch # :nodoc:
private
+ ContentTypeHeader = Struct.new :mime_type, :charset
+ NullContentTypeHeader = ContentTypeHeader.new nil, nil
+
+ def parse_content_type
+ content_type = get_header CONTENT_TYPE
+ if content_type
+ type, charset = content_type.split(/;\s*charset=/)
+ type = nil if type.empty?
+ ContentTypeHeader.new(type, charset)
+ else
+ NullContentTypeHeader
+ end
+ end
+
+ def set_content_type(content_type, charset)
+ type = (content_type || '').dup
+ type << "; charset=#{charset}" if charset
+ set_header CONTENT_TYPE, type
+ end
+
def before_committed
return if committed?
assign_default_content_type_and_charset!
@@ -341,18 +358,11 @@ module ActionDispatch # :nodoc:
end
def assign_default_content_type_and_charset!
- return if self[CONTENT_TYPE].present?
-
- @content_type ||= Mime::HTML
-
- type = @content_type.to_s.dup
- type << "; charset=#{charset}" if append_charset?
+ return if content_type
- self[CONTENT_TYPE] = type
- end
-
- def append_charset?
- !@sending_file && @charset != false
+ ct = parse_content_type
+ set_content_type(ct.mime_type || Mime::HTML.to_s,
+ ct.charset || self.class.default_charset)
end
class RackBody
@@ -392,10 +402,9 @@ module ActionDispatch # :nodoc:
end
def rack_response(status, header)
- header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
-
if NO_CONTENT_CODES.include?(@status)
header.delete CONTENT_TYPE
+ header.delete 'Content-Length'
[status, header, []]
else
[status, header, RackBody.new(self)]
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 6fcf49030b..92b10b6d3b 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,11 +1,10 @@
require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/hash/slice'
module ActionDispatch
module Http
module URL
IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
- HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/
+ HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
mattr_accessor :tld_length
@@ -184,7 +183,7 @@ module ActionDispatch
end
end
- def initialize(env)
+ def initialize
super
@protocol = nil
@port = nil
@@ -229,10 +228,10 @@ module ActionDispatch
# req = Request.new 'HTTP_HOST' => 'example.com:8080'
# req.raw_host_with_port # => "example.com:8080"
def raw_host_with_port
- if forwarded = env["HTTP_X_FORWARDED_HOST"].presence
+ if forwarded = x_forwarded_host.presence
forwarded.split(/,\s?/).last
else
- env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
+ get_header('HTTP_HOST') || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
end
end
@@ -348,7 +347,7 @@ module ActionDispatch
end
def server_port
- @env['SERVER_PORT'].to_i
+ get_header('SERVER_PORT').to_i
end
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index c19ff0f4db..0323360faa 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -33,7 +33,7 @@ module ActionDispatch
defaults = route.defaults
required_parts = route.required_parts
parameterized_parts.keep_if do |key, value|
- defaults[key].nil? || value.to_s != defaults[key].to_s || required_parts.include?(key)
+ (defaults[key].nil? && value.present?) || value.to_s != defaults[key].to_s || required_parts.include?(key)
end
return [route.format(parameterized_parts), params]
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index cf4f654ed6..b653e4eacd 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,55 +1,55 @@
require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/object/blank'
require 'active_support/key_generator'
require 'active_support/message_verifier'
require 'active_support/json'
module ActionDispatch
- class Request < Rack::Request
+ class Request
def cookie_jar
- env['action_dispatch.cookies'.freeze] ||= Cookies::CookieJar.build(self, cookies)
+ fetch_header('action_dispatch.cookies'.freeze) do
+ self.cookie_jar = Cookies::CookieJar.build(self, cookies)
+ end
end
# :stopdoc:
def have_cookie_jar?
- env.key? 'action_dispatch.cookies'.freeze
+ has_header? 'action_dispatch.cookies'.freeze
end
def cookie_jar=(jar)
- env['action_dispatch.cookies'.freeze] = jar
+ set_header 'action_dispatch.cookies'.freeze, jar
end
def key_generator
- env[Cookies::GENERATOR_KEY]
+ get_header Cookies::GENERATOR_KEY
end
def signed_cookie_salt
- env[Cookies::SIGNED_COOKIE_SALT]
+ get_header Cookies::SIGNED_COOKIE_SALT
end
def encrypted_cookie_salt
- env[Cookies::ENCRYPTED_COOKIE_SALT]
+ get_header Cookies::ENCRYPTED_COOKIE_SALT
end
def encrypted_signed_cookie_salt
- env[Cookies::ENCRYPTED_SIGNED_COOKIE_SALT]
+ get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
end
def secret_token
- env[Cookies::SECRET_TOKEN]
+ get_header Cookies::SECRET_TOKEN
end
def secret_key_base
- env[Cookies::SECRET_KEY_BASE]
+ get_header Cookies::SECRET_KEY_BASE
end
def cookies_serializer
- env[Cookies::COOKIES_SERIALIZER]
+ get_header Cookies::COOKIES_SERIALIZER
end
def cookies_digest
- env[Cookies::COOKIES_DIGEST]
+ get_header Cookies::COOKIES_DIGEST
end
# :startdoc:
end
@@ -221,19 +221,11 @@ module ActionDispatch
end
end
- protected
-
- def request; @parent_jar.request; end
-
private
def upgrade_legacy_signed_cookies?
request.secret_token.present? && request.secret_key_base.present?
end
-
- def key_generator
- request.key_generator
- end
end
# Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
@@ -253,6 +245,11 @@ module ActionDispatch
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
+
+ private
+ def parse(name, signed_message)
+ super || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ end
end
class CookieJar #:nodoc:
@@ -319,6 +316,13 @@ module ActionDispatch
self
end
+ def update_cookies_from_jar
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
+ set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) }
+
+ @cookies.update set_cookies if set_cookies
+ end
+
def to_header
@cookies.map { |k,v| "#{k}=#{v}" }.join ';'
end
@@ -405,7 +409,7 @@ module ActionDispatch
end
end
- class PermanentCookieJar #:nodoc:
+ class AbstractCookieJar # :nodoc:
include ChainedCookieJars
def initialize(parent_jar)
@@ -413,19 +417,35 @@ module ActionDispatch
end
def [](name)
- @parent_jar[name.to_s]
+ if data = @parent_jar[name.to_s]
+ parse name, data
+ end
end
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
else
- options = { :value => options }
+ options = { value: options }
end
- options[:expires] = 20.years.from_now
+ commit(options)
@parent_jar[name] = options
end
+
+ protected
+ def request; @parent_jar.request; end
+
+ private
+ def parse(name, data); data; end
+ def commit(options); end
+ end
+
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
+ private
+ def commit(options)
+ options[:expires] = 20.years.from_now
+ end
end
class JsonSerializer # :nodoc:
@@ -477,45 +497,30 @@ module ActionDispatch
def digest
request.cookies_digest || 'SHA1'
end
+
+ def key_generator
+ request.key_generator
+ end
end
- class SignedCookieJar #:nodoc:
- include ChainedCookieJars
+ class SignedCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
- @parent_jar = parent_jar
+ super
secret = key_generator.generate_key(request.signed_cookie_salt)
@verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
- # Returns the value of the cookie by +name+ if it is untampered,
- # returns +nil+ otherwise or if no such cookie exists.
- def [](name)
- if signed_message = @parent_jar[name]
- deserialize name, verify(signed_message)
+ private
+ def parse(name, signed_message)
+ deserialize name, @verifier.verified(signed_message)
end
- end
- # Signs and 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)
- if options.is_a?(Hash)
- options.symbolize_keys!
+ def commit(options)
options[:value] = @verifier.generate(serialize(options[:value]))
- else
- options = { :value => @verifier.generate(serialize(options)) }
- end
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
- @parent_jar[name] = options
- end
-
- private
- def verify(signed_message)
- @verifier.verify(signed_message)
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
end
end
@@ -525,20 +530,13 @@ module ActionDispatch
# re-saves them using the new key generator to provide a smooth upgrade path.
class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
include VerifyAndUpgradeLegacySignedMessage
-
- def [](name)
- if signed_message = @parent_jar[name]
- deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
- end
- end
end
- class EncryptedCookieJar #:nodoc:
- include ChainedCookieJars
+ class EncryptedCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
- @parent_jar = parent_jar
+ super
if ActiveSupport::LegacyKeyGenerator === key_generator
raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
@@ -550,35 +548,18 @@ module ActionDispatch
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
- # Returns the value of the cookie by +name+ if it is untampered,
- # returns +nil+ otherwise or if no such cookie exists.
- def [](name)
- if encrypted_message = @parent_jar[name]
- deserialize name, decrypt_and_verify(encrypted_message)
- end
- end
-
- # Encrypts and 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)
- if options.is_a?(Hash)
- options.symbolize_keys!
- else
- options = { :value => options }
- end
-
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
-
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
- @parent_jar[name] = options
- end
-
private
- def decrypt_and_verify(encrypted_message)
- @encryptor.decrypt_and_verify(encrypted_message)
+ def parse(name, encrypted_message)
+ deserialize name, @encryptor.decrypt_and_verify(encrypted_message)
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
nil
end
+
+ def commit(options)
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
+
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
+ end
end
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
@@ -587,12 +568,6 @@ module ActionDispatch
# encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
include VerifyAndUpgradeLegacySignedMessage
-
- def [](name)
- if encrypted_or_signed_message = @parent_jar[name]
- deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
- end
- end
end
def initialize(app)
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 226a688fe2..66bb74b9c5 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -55,18 +55,17 @@ module ActionDispatch
response
rescue Exception => exception
raise exception unless request.show_exceptions?
- render_exception(env, exception)
+ render_exception(request, exception)
end
private
- def render_exception(env, exception)
- backtrace_cleaner = env['action_dispatch.backtrace_cleaner']
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header('action_dispatch.backtrace_cleaner')
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
- log_error(env, wrapper)
+ log_error(request, wrapper)
- if env['action_dispatch.show_detailed_exceptions']
- request = Request.new(env)
+ if request.get_header('action_dispatch.show_detailed_exceptions')
traces = wrapper.traces
trace_to_show = 'Application Trace'
@@ -108,8 +107,8 @@ module ActionDispatch
[status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
end
- def log_error(env, wrapper)
- logger = logger(env)
+ def log_error(request, wrapper)
+ logger = logger(request)
return unless logger
exception = wrapper.exception
@@ -125,8 +124,8 @@ module ActionDispatch
end
end
- def logger(env)
- env['action_dispatch.logger'] || stderr_logger
+ def logger(request)
+ request.logger || stderr_logger
end
def stderr_logger
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 23da169b22..c482b1c5e7 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,20 +1,22 @@
require 'active_support/core_ext/hash/keys'
module ActionDispatch
- class Request < Rack::Request
+ class Request
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash
- @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"])
+ flash = flash_hash
+ return flash if flash
+ self.flash = Flash::FlashHash.from_session_value(session["flash"])
end
def flash=(flash)
- @env[Flash::KEY] = flash
+ set_header Flash::KEY, flash
end
def flash_hash # :nodoc:
- @env[Flash::KEY]
+ get_header Flash::KEY
end
end
@@ -274,7 +276,7 @@ module ActionDispatch
req = ActionDispatch::Request.new env
@app.call(env)
ensure
- session = Request::Session.find(env) || {}
+ session = Request::Session.find(req) || {}
flash_hash = req.flash_hash
if flash_hash && (flash_hash.present? || session.key?('flash'))
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
index 402ad778fa..9cde9c9b98 100644
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -37,7 +37,9 @@ module ActionDispatch
def call(env)
request = Request.new(env)
- request.request_parameters = parse_formatted_parameters(request, @parsers)
+ parse_formatted_parameters(request, @parsers) do |params|
+ request.request_parameters = params
+ end
@app.call(env)
end
@@ -48,7 +50,7 @@ module ActionDispatch
strategy = parsers.fetch(request.content_mime_type) { return nil }
- strategy.call(request.raw_post)
+ yield strategy.call(request.raw_post)
rescue => e # JSON or Ruby code block errors
logger(request).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 7cde76b30e..0f27984550 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -17,8 +17,8 @@ module ActionDispatch
end
def call(env)
- status = env["PATH_INFO"][1..-1].to_i
request = ActionDispatch::Request.new(env)
+ status = request.path_info[1..-1].to_i
content_type = request.formats.first
body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index 84df55fd5a..9e50fea3fc 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -36,6 +36,11 @@ module ActionDispatch
@default_options.delete(:sidbits)
@default_options.delete(:secure_random)
end
+
+ private
+ def make_request(env)
+ ActionDispatch::Request.new env
+ end
end
module StaleSessionCheck
@@ -65,8 +70,8 @@ module ActionDispatch
end
module SessionObject # :nodoc:
- def prepare_session(env)
- Request::Session.create(self, env, @default_options)
+ def prepare_session(req)
+ Request::Session.create(self, req, @default_options)
end
def loaded_session?(session)
@@ -74,15 +79,14 @@ module ActionDispatch
end
end
- class AbstractStore < Rack::Session::Abstract::ID
+ class AbstractStore < Rack::Session::Abstract::Persisted
include Compatibility
include StaleSessionCheck
include SessionObject
private
- def set_cookie(env, session_id, cookie)
- request = ActionDispatch::Request.new(env)
+ def set_cookie(request, session_id, cookie)
request.cookie_jar[key] = cookie
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index 857e49a682..589ae46e38 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -18,7 +18,7 @@ module ActionDispatch
end
# Get a session from the cache.
- def get_session(env, sid)
+ def find_session(env, sid)
unless sid and session = @cache.read(cache_key(sid))
sid, session = generate_sid, {}
end
@@ -26,7 +26,7 @@ module ActionDispatch
end
# Set a session in the cache.
- def set_session(env, sid, session, options)
+ def write_session(env, sid, session, options)
key = cache_key(sid)
if session
@cache.write(key, session, :expires_in => options[:expire_after])
@@ -37,7 +37,7 @@ module ActionDispatch
end
# Remove a session from the cache.
- def destroy_session(env, sid, options)
+ def delete_session(env, sid, options)
@cache.delete(cache_key(sid))
generate_sid
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index d8f9614904..02b6cfe727 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -53,7 +53,7 @@ module ActionDispatch
#
# Note that changing the secret key will invalidate all existing sessions!
#
- # Because CookieStore extends Rack::Session::Abstract::ID, many of the
+ # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
# options described there can be used to customize the session cookie that
# is generated. For example:
#
@@ -62,7 +62,7 @@ module ActionDispatch
# would set the session cookie to expire automatically 14 days after creation.
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
- class CookieStore < Rack::Session::Abstract::ID
+ class CookieStore < Rack::Session::Abstract::Persisted
include Compatibility
include StaleSessionCheck
include SessionObject
@@ -71,16 +71,16 @@ module ActionDispatch
super(app, options.merge!(:cookie_only => true))
end
- def destroy_session(env, session_id, options)
+ def delete_session(req, session_id, options)
new_sid = generate_sid unless options[:drop]
# Reset hash and Assign the new session id
- env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
new_sid
end
- def load_session(env)
+ def load_session(req)
stale_session_check! do
- data = unpacked_cookie_data(env)
+ data = unpacked_cookie_data(req)
data = persistent_session_id!(data)
[data["session_id"], data]
end
@@ -88,20 +88,21 @@ module ActionDispatch
private
- def extract_session_id(env)
+ def extract_session_id(req)
stale_session_check! do
- unpacked_cookie_data(env)["session_id"]
+ unpacked_cookie_data(req)["session_id"]
end
end
- def unpacked_cookie_data(env)
- env["action_dispatch.request.unsigned_session_cookie"] ||= begin
- stale_session_check! do
- if data = get_cookie(env)
+ def unpacked_cookie_data(req)
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
+ v = stale_session_check! do
+ if data = get_cookie(req)
data.stringify_keys!
end
data || {}
end
+ req.set_header k, v
end
end
@@ -111,21 +112,20 @@ module ActionDispatch
data
end
- def set_session(env, sid, session_data, options)
+ def write_session(req, sid, session_data, options)
session_data["session_id"] = sid
session_data
end
- def set_cookie(env, session_id, cookie)
- cookie_jar(env)[@key] = cookie
+ def set_cookie(request, session_id, cookie)
+ cookie_jar(request)[@key] = cookie
end
- def get_cookie(env)
- cookie_jar(env)[@key]
+ def get_cookie(req)
+ cookie_jar(req)[@key]
end
- def cookie_jar(env)
- request = ActionDispatch::Request.new(env)
+ def cookie_jar(request)
request.cookie_jar.signed_or_encrypted
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index 12d8dab8eb..64695f9738 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -31,7 +31,7 @@ module ActionDispatch
@app.call(env)
rescue Exception => exception
if request.show_exceptions?
- render_exception(env, exception)
+ render_exception(request, exception)
else
raise exception
end
@@ -39,14 +39,14 @@ module ActionDispatch
private
- def render_exception(env, exception)
- backtrace_cleaner = env['action_dispatch.backtrace_cleaner']
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner'
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
status = wrapper.status_code
- env["action_dispatch.exception"] = wrapper.exception
- env["action_dispatch.original_path"] = env["PATH_INFO"]
- env["PATH_INFO"] = "/#{status}"
- response = @exceptions_app.call(env)
+ request.set_header "action_dispatch.exception", wrapper.exception
+ request.set_header "action_dispatch.original_path", request.path_info
+ request.path_info = "/#{status}"
+ response = @exceptions_app.call(request.env)
response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
rescue Exception => failsafe_error
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 7b3d8bcc5b..b72953f1d1 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -1,72 +1,128 @@
module ActionDispatch
+ # This middleware is added to the stack when `config.force_ssl = true`.
+ # It does three jobs to enforce secure HTTP requests:
+ #
+ # 1. TLS redirect. http:// requests are permanently redirected to https://
+ # with the same URL host, path, etc. Pass `:host` and/or `:port` to
+ # modify the destination URL. This is always enabled.
+ #
+ # 2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they
+ # mustn't be sent along with http:// requests. This is always enabled.
+ #
+ # 3. HTTP Strict Transport Security (HSTS). Tells the browser to remember
+ # this site as TLS-only and automatically redirect non-TLS requests.
+ # Enabled by default. Pass `hsts: false` to disable.
+ #
+ # Configure HSTS with `hsts: { … }`:
+ # * `expires`: How long, in seconds, these settings will stick. Defaults to
+ # `18.weeks`, the minimum required to qualify for browser preload lists.
+ # * `subdomains`: Set to `true` to tell the browser to apply these settings
+ # to all subdomains. This protects your cookies from interception by a
+ # vulnerable site on a subdomain. Defaults to `false`.
+ # * `preload`: Advertise that this site may be included in browsers'
+ # preloaded HSTS lists. HSTS protects your site on every visit *except the
+ # first visit* since it hasn't seen your HSTS header yet. To close this
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
+ # Go to https://hstspreload.appspot.com to submit your site for inclusion.
+ #
+ # Disabling HSTS: To turn off HSTS, omitting the header is not enough.
+ # Browsers will remember the original HSTS directive until it expires.
+ # Instead, use the header to tell browsers to expire HSTS immediately.
+ # Setting `hsts: false` is a shortcut for `hsts: { expires: 0 }`.
class SSL
- YEAR = 31536000
+ # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
+ # and greater than the 18-week requirement for browser preload lists.
+ HSTS_EXPIRES_IN = 15552000
def self.default_hsts_options
- { :expires => YEAR, :subdomains => false }
+ { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }
end
- def initialize(app, options = {})
+ def initialize(app, redirect: {}, hsts: {}, **options)
@app = app
- @hsts = options.fetch(:hsts, {})
- @hsts = {} if @hsts == true
- @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
+ if options[:host] || options[:port]
+ ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
+ The `:host` and `:port` options are moving within `:redirect`:
+ `config.ssl_options = { redirect: { host: …, port: … }}`.
+ end_warning
+ @redirect = options.slice(:host, :port)
+ else
+ @redirect = redirect
+ end
- @host = options[:host]
- @port = options[:port]
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
end
def call(env)
- request = Request.new(env)
+ request = Request.new env
if request.ssl?
- status, headers, body = @app.call(env)
- headers.reverse_merge!(hsts_headers)
- flag_cookies_as_secure!(headers)
- [status, headers, body]
+ @app.call(env).tap do |status, headers, body|
+ set_hsts_header! headers
+ flag_cookies_as_secure! headers
+ end
else
- redirect_to_https(request)
+ redirect_to_https request
end
end
private
- def redirect_to_https(request)
- host = @host || request.host
- port = @port || request.port
-
- location = "https://#{host}"
- location << ":#{port}" if port != 80
- location << request.fullpath
-
- headers = { 'Content-Type' => 'text/html', 'Location' => location }
-
- [301, headers, []]
+ def set_hsts_header!(headers)
+ headers['Strict-Transport-Security'.freeze] ||= @hsts_header
end
- # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
- def hsts_headers
- if @hsts
- value = "max-age=#{@hsts[:expires].to_i}"
- value += "; includeSubDomains" if @hsts[:subdomains]
- { 'Strict-Transport-Security' => value }
+ def normalize_hsts_options(options)
+ case options
+ # Explicitly disabling HSTS clears the existing setting from browsers
+ # by setting expiry to 0.
+ when false
+ self.class.default_hsts_options.merge(expires: 0)
+ # Default to enabled, with default options.
+ when nil, true
+ self.class.default_hsts_options
else
- {}
+ self.class.default_hsts_options.merge(options)
end
end
+ # http://tools.ietf.org/html/rfc6797#section-6.1
+ def build_hsts_header(hsts)
+ value = "max-age=#{hsts[:expires].to_i}"
+ value << "; includeSubDomains" if hsts[:subdomains]
+ value << "; preload" if hsts[:preload]
+ value
+ end
+
def flag_cookies_as_secure!(headers)
- if cookies = headers['Set-Cookie']
- cookies = cookies.split("\n")
+ if cookies = headers['Set-Cookie'.freeze]
+ cookies = cookies.split("\n".freeze)
- headers['Set-Cookie'] = cookies.map { |cookie|
+ headers['Set-Cookie'.freeze] = cookies.map { |cookie|
if cookie !~ /;\s*secure\s*(;|$)/i
"#{cookie}; secure"
else
cookie
end
- }.join("\n")
+ }.join("\n".freeze)
end
end
+
+ def redirect_to_https(request)
+ [ @redirect.fetch(:status, 301),
+ { 'Content-Type' => 'text/html',
+ 'Location' => https_location_for(request) },
+ @redirect.fetch(:body, []) ]
+ end
+
+ def https_location_for(request)
+ host = @redirect[:host] || request.host
+ port = @redirect[:port] || request.port
+
+ location = "https://#{host}"
+ location << ":#{port}" if port != 80 && port != 443
+ location << request.fullpath
+ location
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index 0430ce3b9a..90e2ae6802 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -87,7 +87,7 @@ module ActionDispatch
middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
end
- protected
+ private
def assert_index(index, where)
index = get_class index
@@ -96,8 +96,6 @@ module ActionDispatch
i
end
- private
-
def get_class(klass)
if klass.is_a?(String) || klass.is_a?(Symbol)
classcache = ActiveSupport::Dependencies::Reference
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 9462ae4278..c4344c9609 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -28,7 +28,7 @@ module ActionDispatch
# Used by the `Static` class to check the existence of a valid file
# in the server's `public/` directory (see Static#call).
def match?(path)
- path = URI.parser.unescape(path)
+ path = ::Rack::Utils.unescape_path path
return false unless path.valid_encoding?
path = Rack::Utils.clean_path_info path
@@ -43,7 +43,7 @@ module ActionDispatch
end
}
- return ::Rack::Utils.escape(match)
+ return ::Rack::Utils.escape_path(match)
end
end
@@ -90,7 +90,7 @@ module ActionDispatch
def gzip_file_path(path)
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
gzip_path = "#{path}.gz"
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
gzip_path
else
false
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index bae3d909fa..9e7fcbd849 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -1,7 +1,7 @@
require 'rack/session/abstract/id'
module ActionDispatch
- class Request < Rack::Request
+ class Request
# Session is responsible for lazily loading the session from store.
class Session # :nodoc:
ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
@@ -11,31 +11,31 @@ module ActionDispatch
Unspecified = Object.new
# Creates a session hash, merging the properties of the previous session if any
- def self.create(store, env, default_options)
- session_was = find env
- session = Request::Session.new(store, env)
+ def self.create(store, req, default_options)
+ session_was = find req
+ session = Request::Session.new(store, req)
session.merge! session_was if session_was
- set(env, session)
- Options.set(env, Request::Session::Options.new(store, default_options))
+ set(req, session)
+ Options.set(req, Request::Session::Options.new(store, default_options))
session
end
- def self.find(env)
- env[ENV_SESSION_KEY]
+ def self.find(req)
+ req.get_header ENV_SESSION_KEY
end
- def self.set(env, session)
- env[ENV_SESSION_KEY] = session
+ def self.set(req, session)
+ req.set_header ENV_SESSION_KEY, session
end
class Options #:nodoc:
- def self.set(env, options)
- env[ENV_SESSION_OPTIONS_KEY] = options
+ def self.set(req, options)
+ req.set_header ENV_SESSION_OPTIONS_KEY, options
end
- def self.find(env)
- env[ENV_SESSION_OPTIONS_KEY]
+ def self.find(req)
+ req.get_header ENV_SESSION_OPTIONS_KEY
end
def initialize(by, default_options)
@@ -47,9 +47,9 @@ module ActionDispatch
@delegate[key]
end
- def id(env)
+ def id(req)
@delegate.fetch(:id) {
- @by.send(:extract_session_id, env)
+ @by.send(:extract_session_id, req)
}
end
@@ -58,26 +58,26 @@ module ActionDispatch
def values_at(*args); @delegate.values_at(*args); end
end
- def initialize(by, env)
+ def initialize(by, req)
@by = by
- @env = env
+ @req = req
@delegate = {}
@loaded = false
@exists = nil # we haven't checked yet
end
def id
- options.id(@env)
+ options.id(@req)
end
def options
- Options.find @env
+ Options.find @req
end
def destroy
clear
options = self.options || {}
- @by.send(:destroy_session, @env, options.id(@env), options)
+ @by.send(:delete_session, @req, options.id(@req), options)
# Load the new sid to be written with the response
@loaded = false
@@ -181,7 +181,7 @@ module ActionDispatch
def exists?
return @exists unless @exists.nil?
- @exists = @by.send(:session_exists?, @env)
+ @exists = @by.send(:session_exists?, @req)
end
def loaded?
@@ -209,7 +209,7 @@ module ActionDispatch
end
def load!
- id, session = @by.load_session @env
+ id, session = @by.load_session @req
options[:id] = id
@delegate.replace(stringify_keys(session))
@loaded = true
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index 3973ea6346..a8151a8224 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -1,5 +1,5 @@
module ActionDispatch
- class Request < Rack::Request
+ class Request
class Utils # :nodoc:
mattr_accessor :perform_deep_munge
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index a42cf72f60..8757c9ea7f 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,7 +1,4 @@
# encoding: UTF-8
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/regexp'
-require 'active_support/dependencies/autoload'
module ActionDispatch
# The routing module provides URL rewriting in native Ruby. It's a way to
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 51bafb539f..87b826f7d0 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,10 +1,8 @@
-require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/inflector'
+require 'active_support/core_ext/regexp'
require 'active_support/deprecation'
require 'action_dispatch/routing/redirection'
require 'action_dispatch/routing/endpoint'
@@ -283,12 +281,16 @@ module ActionDispatch
end
def app(blocks)
- if to.respond_to?(:call)
- Constraints.new(to, blocks, Constraints::CALL)
- elsif blocks.any?
- Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
+ if to.is_a?(Class) && to < ActionController::Metal
+ Routing::RouteSet::StaticDispatcher.new to
else
- dispatcher(defaults.key?(:controller))
+ if to.respond_to?(:call)
+ Constraints.new(to, blocks, Constraints::CALL)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
+ else
+ dispatcher(defaults.key?(:controller))
+ end
end
end
@@ -368,7 +370,7 @@ module ActionDispatch
end
def dispatcher(raise_on_name_error)
- @set.dispatcher raise_on_name_error
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
end
end
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index c298080ac8..e4b8d5993e 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -27,56 +27,38 @@ module ActionDispatch
def dispatcher?; true; end
def serve(req)
- req.check_path_parameters!
- params = req.path_parameters
-
- prepare_params!(params)
-
- controller = controller(params, @raise_on_name_error) do
+ params = req.path_parameters
+ controller = controller req
+ res = controller.make_response! req
+ dispatch(controller, params[:action], req, res)
+ rescue NameError => e
+ if @raise_on_name_error
+ raise ActionController::RoutingError, e.message, e.backtrace
+ else
return [404, {'X-Cascade' => 'pass'}, []]
end
-
- dispatch(controller, params[:action], req)
end
- def prepare_params!(params)
- normalize_controller!(params)
- merge_default_action!(params)
- end
+ private
- # If this is a default_controller (i.e. a controller specified by the user)
- # we should raise an error in case it's not found, because it usually means
- # a user error. However, if the controller was retrieved through a dynamic
- # segment, as in :controller(/:action), we should simply return nil and
- # delegate the control back to Rack cascade. Besides, if this is not a default
- # controller, it means we should respect the @scope[:module] parameter.
- def controller(params, raise_on_name_error=true)
- controller_reference params.fetch(:controller) { yield }
- rescue NameError => e
- raise ActionController::RoutingError, e.message, e.backtrace if raise_on_name_error
- yield
+ def controller(req)
+ req.controller_class
end
- protected
-
- def controller_reference(controller_param)
- const_name = "#{controller_param.camelize}Controller"
- ActiveSupport::Dependencies.constantize(const_name)
+ def dispatch(controller, action, req, res)
+ controller.dispatch(action, req, res)
end
+ end
- private
-
- def dispatch(controller, action, req)
- controller.action(action).call(req.env)
+ class StaticDispatcher < Dispatcher
+ def initialize(controller_class)
+ super(false)
+ @controller_class = controller_class
end
- def normalize_controller!(params)
- params[:controller] = params[:controller].underscore if params.key?(:controller)
- end
+ private
- def merge_default_action!(params)
- params[:action] ||= 'index'
- end
+ def controller(_); @controller_class; end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -198,9 +180,9 @@ module ActionDispatch
private
def optimized_helper(args)
- params = parameterize_args(args) { |k|
+ params = parameterize_args(args) do
raise_generation_error(args)
- }
+ end
@route.format params
end
@@ -312,7 +294,7 @@ module ActionDispatch
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
- attr_accessor :default_url_options, :dispatcher_class
+ attr_accessor :default_url_options
attr_reader :env_key
alias :routes :set
@@ -355,7 +337,6 @@ module ActionDispatch
@set = Journey::Routes.new
@router = Journey::Router.new @set
@formatter = Journey::Formatter.new self
- @dispatcher_class = Routing::RouteSet::Dispatcher
end
def relative_url_root
@@ -370,6 +351,11 @@ module ActionDispatch
ActionDispatch::Request
end
+ def make_request(env)
+ request_class.new env
+ end
+ private :make_request
+
def draw(&block)
clear! unless @disable_clear_and_finalize
eval_block(block)
@@ -413,10 +399,6 @@ module ActionDispatch
@prepend.each { |blk| eval_block(blk) }
end
- def dispatcher(raise_on_name_error)
- dispatcher_class.new(raise_on_name_error)
- end
-
module MountedHelpers
extend ActiveSupport::Concern
include UrlFor
@@ -727,7 +709,7 @@ module ActionDispatch
end
def call(env)
- req = request_class.new(env)
+ req = make_request(env)
req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
@router.serve(req)
end
@@ -743,7 +725,7 @@ module ActionDispatch
raise ActionController::RoutingError, e.message
end
- req = request_class.new(env)
+ req = make_request(env)
@router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
@@ -756,14 +738,13 @@ module ActionDispatch
req.path_parameters = old_params.merge params
app = route.app
if app.matches?(req) && app.dispatcher?
- dispatcher = app.app
-
- dispatcher.controller(params, false) do
+ begin
+ req.controller_class
+ rescue NameError
raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
end
- dispatcher.prepare_params!(params)
- return params
+ return req.path_parameters
end
end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 967bbd62f8..883cd9c2c3 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -180,7 +180,8 @@ module ActionDispatch
when Symbol
HelperMethodBuilder.url.handle_string_call self, options
when Array
- polymorphic_url(options, options.extract_options!)
+ components = options.dup
+ polymorphic_url(components, components.extract_options!)
when Class
HelperMethodBuilder.url.handle_class_call self, options
else