Skip to content

Commit d25fedd

Browse files
authored
Fix handling of cached values in Rack::Request. (#2054)
* Per-class cache keys for cached query/body parameters. * Use the query parser class as the default cache key.
1 parent 95d2f64 commit d25fedd

File tree

2 files changed

+171
-49
lines changed

2 files changed

+171
-49
lines changed

lib/rack/request.rb

Lines changed: 110 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -480,25 +480,114 @@ def parseable_data?
480480
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
481481
end
482482

483-
# Returns the data received in the query string.
484-
def GET
485-
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
486-
if query_hash = get_header(RACK_REQUEST_QUERY_HASH)
487-
return query_hash
483+
# Given a current input value, and a validity key, check if the cache
484+
# is valid, and if so, return the cached value. If not, yield the
485+
# current value to the block, and set the cache to the result.
486+
#
487+
# This method does not use cache_key, so it is shared between all
488+
# instance of Rack::Request and it's sub-classes.
489+
private def cache_for(key, validity_key, current_value)
490+
# Get the current value of the validity key and compare it with the input value:
491+
if get_header(validity_key).equal?(current_value)
492+
# If the values are the same, then the cache is valid, so return the cached value.
493+
if has_header?(key)
494+
value = get_header(key)
495+
# If the cached value is an exception, then re-raise it.
496+
if value.is_a?(Exception)
497+
raise value.class, value.message, cause: value.cause
498+
else
499+
# Otherwise, return the cached value.
500+
return value
501+
end
488502
end
489503
end
490504

491-
set_header(RACK_REQUEST_QUERY_HASH, expand_params(query_param_list))
505+
# If the cache is not valid, then yield the current value to the block:
506+
value = yield(current_value)
507+
508+
# Set the validity key to the current value so that we can detect changes:
509+
set_header(validity_key, current_value)
510+
511+
# Set the cache to the result of the block, and return the result:
512+
set_header(key, value)
513+
rescue => error
514+
# If an exception is raised, then set the cache to the exception, and re-raise it:
515+
set_header(validity_key, current_value)
516+
set_header(key, error)
517+
raise
518+
end
519+
520+
# This cache key is used by cached values generated by class_cache_for,
521+
# specfically GET and POST. This is to ensure that the cache is not
522+
# shared between instances of different classes which have different
523+
# behaviour. This includes sub-classes that override query_parser or
524+
# expand_params.
525+
def cache_key
526+
query_parser.class
527+
end
528+
529+
# Given a current input value, and a validity key, check if the cache
530+
# is valid, and if so, return the cached value. If not, yield the
531+
# current value to the block, and set the cache to the result.
532+
#
533+
# This method uses cache_key to ensure that the cache is not shared
534+
# between instances of different classes which have different
535+
# behaviour of the cached operations.
536+
private def class_cache_for(key, validity_key, current_value)
537+
# The cache is organised in the env as:
538+
# env[key][cache_key] = value
539+
# and is valid as long as env[validity_key].equal?(current_value)
540+
541+
cache_key = self.cache_key
542+
543+
# Get the current value of the validity key and compare it with the input value:
544+
if get_header(validity_key).equal?(current_value)
545+
# Lookup the cache for the current cache key:
546+
if cache = get_header(key)
547+
if cache.key?(cache_key)
548+
# If the cache is valid, then return the cached value.
549+
value = cache[cache_key]
550+
if value.is_a?(Exception)
551+
# If the cached value is an exception, then re-raise it.
552+
raise value.class, value.message, cause: value.cause
553+
else
554+
# Otherwise, return the cached value.
555+
return value
556+
end
557+
end
558+
end
559+
end
560+
561+
# If the cache was not defined for this cache key, then create a new cache:
562+
unless cache
563+
set_header(key, cache = {})
564+
end
565+
566+
begin
567+
# Yield the current value to the block to generate an updated value:
568+
value = yield(current_value)
569+
570+
# Only set this after generating the value, so that if an error or other cache depending on the same key, it will be invalidated correctly:
571+
set_header(validity_key, current_value)
572+
return cache[cache_key] = value
573+
rescue => error
574+
set_header(validity_key, current_value)
575+
cache[cache_key] = error
576+
raise
577+
end
578+
end
579+
580+
# Returns the data received in the query string.
581+
def GET
582+
class_cache_for(RACK_REQUEST_QUERY_HASH, RACK_REQUEST_QUERY_STRING, query_string) do
583+
expand_params(query_param_list)
584+
end
492585
end
493586

494587
def query_param_list
495-
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
496-
get_header(RACK_REQUEST_QUERY_PAIRS)
497-
else
498-
query_pairs = split_query(query_string, '&')
499-
set_header RACK_REQUEST_QUERY_STRING, query_string
500-
set_header RACK_REQUEST_QUERY_HASH, nil
501-
set_header(RACK_REQUEST_QUERY_PAIRS, query_pairs)
588+
cache_for(RACK_REQUEST_QUERY_PAIRS, RACK_REQUEST_QUERY_STRING, query_string) do
589+
set_header(RACK_REQUEST_QUERY_HASH, nil)
590+
split_query(query_string, '&')
502591
end
503592
end
504593

@@ -507,33 +596,13 @@ def query_param_list
507596
# This method support both application/x-www-form-urlencoded and
508597
# multipart/form-data.
509598
def POST
510-
if get_header(RACK_REQUEST_FORM_INPUT).equal?(get_header(RACK_INPUT))
511-
if form_hash = get_header(RACK_REQUEST_FORM_HASH)
512-
return form_hash
513-
end
599+
class_cache_for(RACK_REQUEST_FORM_HASH, RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)) do
600+
expand_params(body_param_list)
514601
end
515-
516-
set_header(RACK_REQUEST_FORM_HASH, expand_params(body_param_list))
517602
end
518603

519604
def body_param_list
520-
if error = get_header(RACK_REQUEST_FORM_ERROR)
521-
raise error.class, error.message, cause: error.cause
522-
end
523-
524-
begin
525-
rack_input = get_header(RACK_INPUT)
526-
527-
form_pairs = nil
528-
529-
# If the form data has already been memoized from the same
530-
# input:
531-
if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
532-
if form_pairs = get_header(RACK_REQUEST_FORM_PAIRS)
533-
return form_pairs
534-
end
535-
end
536-
605+
cache_for(RACK_REQUEST_FORM_PAIRS, RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)) do |rack_input|
537606
if rack_input.nil?
538607
form_pairs = []
539608
elsif form_data? || parseable_data?
@@ -544,19 +613,16 @@ def body_param_list
544613
# form_vars.sub!(/\0\z/, '') # performance replacement:
545614
form_vars.slice!(-1) if form_vars.end_with?("\0")
546615

547-
set_header RACK_REQUEST_FORM_VARS, form_vars
616+
# Removing this line breaks Rail test "test_filters_rack_request_form_vars"!
617+
set_header(RACK_REQUEST_FORM_VARS, form_vars)
618+
548619
form_pairs = split_query(form_vars, '&')
549620
end
550621
else
551622
form_pairs = []
552623
end
553-
554-
set_header RACK_REQUEST_FORM_INPUT, rack_input
555-
set_header RACK_REQUEST_FORM_HASH, nil
556-
set_header(RACK_REQUEST_FORM_PAIRS, form_pairs)
557-
rescue => error
558-
set_header(RACK_REQUEST_FORM_ERROR, error)
559-
raise
624+
625+
form_pairs
560626
end
561627
end
562628

test/spec_request.rb

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,12 +1554,19 @@ def initialize(*)
15541554
rack_input.write(input)
15551555
rack_input.rewind
15561556

1557-
req = make_request Rack::MockRequest.env_for("/",
1558-
"rack.request.form_hash" => { 'foo' => 'bar' },
1559-
"rack.request.form_input" => rack_input,
1560-
:input => rack_input)
1557+
form_hash_cache = {}
1558+
1559+
req = make_request Rack::MockRequest.env_for(
1560+
"/",
1561+
"rack.request.form_hash" => form_hash_cache,
1562+
"rack.request.form_input" => rack_input,
1563+
:input => rack_input
1564+
)
15611565

1562-
req.POST.must_equal req.env['rack.request.form_hash']
1566+
form_hash = {'foo' => 'bar'}.freeze
1567+
form_hash_cache[req.cache_key] = form_hash
1568+
1569+
req.POST.must_equal form_hash
15631570
end
15641571

15651572
it "conform to the Rack spec" do
@@ -1957,4 +1964,53 @@ def make_request(env)
19571964
DelegateRequest.new super(env)
19581965
end
19591966
end
1967+
1968+
class UpperRequest < Rack::Request
1969+
def expand_params(parameters)
1970+
parameters.map do |(key, value)|
1971+
[key.upcase, value]
1972+
end.to_h
1973+
end
1974+
1975+
# If this is not specified, the behaviour becomes order dependent.
1976+
def cache_key
1977+
:my_request
1978+
end
1979+
end
1980+
1981+
it "correctly expands parameters" do
1982+
env = {"QUERY_STRING" => "foo=bar"}
1983+
1984+
request = Rack::Request.new(env)
1985+
request.query_param_list.must_equal [["foo", "bar"]]
1986+
request.GET.must_equal "foo" => "bar"
1987+
1988+
upper_request = UpperRequest.new(env)
1989+
upper_request.query_param_list.must_equal [["foo", "bar"]]
1990+
upper_request.GET.must_equal "FOO" => "bar"
1991+
1992+
env['QUERY_STRING'] = "foo=bar&bar=baz"
1993+
1994+
request.GET.must_equal "foo" => "bar", "bar" => "baz"
1995+
upper_request.GET.must_equal "FOO" => "bar", "BAR" => "baz"
1996+
end
1997+
1998+
class BrokenRequest < Rack::Request
1999+
def expand_params(parameters)
2000+
raise "boom"
2001+
end
2002+
end
2003+
2004+
it "raises an error if expand_params raises an error" do
2005+
env = {"QUERY_STRING" => "foo=bar"}
2006+
2007+
request = Rack::Request.new(env)
2008+
request.GET.must_equal "foo" => "bar"
2009+
2010+
broken_request = BrokenRequest.new(env)
2011+
lambda { broken_request.GET }.must_raise RuntimeError
2012+
2013+
# Subsequnt calls also raise an error:
2014+
lambda { broken_request.GET }.must_raise RuntimeError
2015+
end
19602016
end

0 commit comments

Comments
 (0)