actionpack 7.2.1.1 → 8.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -115
  3. data/lib/action_controller/api.rb +1 -0
  4. data/lib/action_controller/metal/conditional_get.rb +6 -3
  5. data/lib/action_controller/metal/http_authentication.rb +6 -3
  6. data/lib/action_controller/metal/instrumentation.rb +1 -2
  7. data/lib/action_controller/metal/live.rb +19 -8
  8. data/lib/action_controller/metal/rate_limiting.rb +13 -4
  9. data/lib/action_controller/metal/renderers.rb +2 -1
  10. data/lib/action_controller/metal/streaming.rb +5 -84
  11. data/lib/action_controller/metal/strong_parameters.rb +274 -73
  12. data/lib/action_controller/railtie.rb +1 -1
  13. data/lib/action_controller/test_case.rb +4 -3
  14. data/lib/action_dispatch/http/cache.rb +27 -10
  15. data/lib/action_dispatch/http/content_security_policy.rb +1 -0
  16. data/lib/action_dispatch/http/filter_parameters.rb +4 -9
  17. data/lib/action_dispatch/http/filter_redirect.rb +2 -9
  18. data/lib/action_dispatch/http/permissions_policy.rb +2 -0
  19. data/lib/action_dispatch/http/request.rb +4 -2
  20. data/lib/action_dispatch/journey/parser.rb +99 -196
  21. data/lib/action_dispatch/journey/scanner.rb +40 -42
  22. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  23. data/lib/action_dispatch/middleware/debug_exceptions.rb +16 -3
  24. data/lib/action_dispatch/middleware/remote_ip.rb +5 -6
  25. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  26. data/lib/action_dispatch/middleware/ssl.rb +14 -4
  27. data/lib/action_dispatch/railtie.rb +2 -0
  28. data/lib/action_dispatch/routing/inspector.rb +1 -1
  29. data/lib/action_dispatch/routing/mapper.rb +26 -17
  30. data/lib/action_dispatch/routing/route_set.rb +18 -6
  31. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  32. data/lib/action_pack/gem_version.rb +4 -4
  33. metadata +12 -34
  34. data/lib/action_dispatch/journey/parser.y +0 -50
  35. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -37,16 +37,9 @@ module ActionDispatch
37
37
  def parameter_filtered_location
38
38
  uri = URI.parse(location)
39
39
  unless uri.query.nil? || uri.query.empty?
40
- parts = uri.query.split(/([&;])/)
41
- filtered_parts = parts.map do |part|
42
- if part.include?("=")
43
- key, value = part.split("=", 2)
44
- request.parameter_filter.filter(key => value).first.join("=")
45
- else
46
- part
47
- end
40
+ uri.query.gsub!(FilterParameters::PAIR_RE) do
41
+ request.parameter_filter.filter($1 => $2).first.join("=")
48
42
  end
49
- uri.query = filtered_parts.join("")
50
43
  end
51
44
  uri.to_s
52
45
  rescue URI::Error
@@ -86,12 +86,14 @@ module ActionDispatch # :nodoc:
86
86
  ambient_light_sensor: "ambient-light-sensor",
87
87
  autoplay: "autoplay",
88
88
  camera: "camera",
89
+ display_capture: "display-capture",
89
90
  encrypted_media: "encrypted-media",
90
91
  fullscreen: "fullscreen",
91
92
  geolocation: "geolocation",
92
93
  gyroscope: "gyroscope",
93
94
  hid: "hid",
94
95
  idle_detection: "idle-detection",
96
+ keyboard_map: "keyboard-map",
95
97
  magnetometer: "magnetometer",
96
98
  microphone: "microphone",
97
99
  midi: "midi",
@@ -55,6 +55,8 @@ module ActionDispatch
55
55
  METHOD
56
56
  end
57
57
 
58
+ TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING" # :nodoc:
59
+
58
60
  def self.empty
59
61
  new({})
60
62
  end
@@ -282,7 +284,7 @@ module ActionDispatch
282
284
 
283
285
  # Returns the content length of the request as an integer.
284
286
  def content_length
285
- return raw_post.bytesize if headers.key?("Transfer-Encoding")
287
+ return raw_post.bytesize if has_header?(TRANSFER_ENCODING)
286
288
  super.to_i
287
289
  end
288
290
 
@@ -468,7 +470,7 @@ module ActionDispatch
468
470
  def read_body_stream
469
471
  if body_stream
470
472
  reset_stream(body_stream) do
471
- if headers.key?("Transfer-Encoding")
473
+ if has_header?(TRANSFER_ENCODING)
472
474
  body_stream.read # Read body stream until EOF if "Transfer-Encoding" is present
473
475
  else
474
476
  body_stream.read(content_length)
@@ -1,200 +1,103 @@
1
- #
2
- # DO NOT MODIFY!!!!
3
- # This file is automatically generated by Racc 1.4.16 from
4
- # Racc grammar file "".
1
+ # frozen_string_literal: true
5
2
 
6
- # :markup: markdown
3
+ require "action_dispatch/journey/scanner"
4
+ require "action_dispatch/journey/nodes/node"
7
5
 
8
- require 'racc/parser.rb'
9
-
10
- # :stopdoc:
11
-
12
- require "action_dispatch/journey/parser_extras"
13
6
  module ActionDispatch
14
- module Journey
15
- class Parser < Racc::Parser
16
- ##### State transition tables begin ###
17
-
18
- racc_action_table = [
19
- 13, 15, 14, 7, 19, 16, 8, 19, 13, 15,
20
- 14, 7, 17, 16, 8, 13, 15, 14, 7, 21,
21
- 16, 8, 13, 15, 14, 7, 24, 16, 8 ]
22
-
23
- racc_action_check = [
24
- 2, 2, 2, 2, 22, 2, 2, 2, 19, 19,
25
- 19, 19, 1, 19, 19, 7, 7, 7, 7, 17,
26
- 7, 7, 0, 0, 0, 0, 20, 0, 0 ]
27
-
28
- racc_action_pointer = [
29
- 20, 12, -2, nil, nil, nil, nil, 13, nil, nil,
30
- nil, nil, nil, nil, nil, nil, nil, 19, nil, 6,
31
- 20, nil, -5, nil, nil ]
32
-
33
- racc_action_default = [
34
- -19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
35
- -12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
36
- -19, 25, -8, -9, -7 ]
37
-
38
- racc_goto_table = [
39
- 1, 22, 18, 23, nil, nil, nil, 20 ]
40
-
41
- racc_goto_check = [
42
- 1, 2, 1, 3, nil, nil, nil, 1 ]
43
-
44
- racc_goto_pointer = [
45
- nil, 0, -18, -16, nil, nil, nil, nil, nil, nil,
46
- nil ]
47
-
48
- racc_goto_default = [
49
- nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
50
- 12 ]
51
-
52
- racc_reduce_table = [
53
- 0, 0, :racc_error,
54
- 2, 11, :_reduce_1,
55
- 1, 11, :_reduce_2,
56
- 1, 11, :_reduce_none,
57
- 1, 12, :_reduce_none,
58
- 1, 12, :_reduce_none,
59
- 1, 12, :_reduce_none,
60
- 3, 15, :_reduce_7,
61
- 3, 13, :_reduce_8,
62
- 3, 13, :_reduce_9,
63
- 1, 16, :_reduce_10,
64
- 1, 14, :_reduce_none,
65
- 1, 14, :_reduce_none,
66
- 1, 14, :_reduce_none,
67
- 1, 14, :_reduce_none,
68
- 1, 19, :_reduce_15,
69
- 1, 17, :_reduce_16,
70
- 1, 18, :_reduce_17,
71
- 1, 20, :_reduce_18 ]
72
-
73
- racc_reduce_n = 19
74
-
75
- racc_shift_n = 25
76
-
77
- racc_token_table = {
78
- false => 0,
79
- :error => 1,
80
- :SLASH => 2,
81
- :LITERAL => 3,
82
- :SYMBOL => 4,
83
- :LPAREN => 5,
84
- :RPAREN => 6,
85
- :DOT => 7,
86
- :STAR => 8,
87
- :OR => 9 }
88
-
89
- racc_nt_base = 10
90
-
91
- racc_use_result_var = false
92
-
93
- Racc_arg = [
94
- racc_action_table,
95
- racc_action_check,
96
- racc_action_default,
97
- racc_action_pointer,
98
- racc_goto_table,
99
- racc_goto_check,
100
- racc_goto_default,
101
- racc_goto_pointer,
102
- racc_nt_base,
103
- racc_reduce_table,
104
- racc_token_table,
105
- racc_shift_n,
106
- racc_reduce_n,
107
- racc_use_result_var ]
108
-
109
- Racc_token_to_s_table = [
110
- "$end",
111
- "error",
112
- "SLASH",
113
- "LITERAL",
114
- "SYMBOL",
115
- "LPAREN",
116
- "RPAREN",
117
- "DOT",
118
- "STAR",
119
- "OR",
120
- "$start",
121
- "expressions",
122
- "expression",
123
- "or",
124
- "terminal",
125
- "group",
126
- "star",
127
- "symbol",
128
- "literal",
129
- "slash",
130
- "dot" ]
131
-
132
- Racc_debug_parser = false
133
-
134
- ##### State transition tables end #####
135
-
136
- # reduce 0 omitted
137
-
138
- def _reduce_1(val, _values)
139
- Cat.new(val.first, val.last)
140
- end
141
-
142
- def _reduce_2(val, _values)
143
- val.first
144
- end
145
-
146
- # reduce 3 omitted
147
-
148
- # reduce 4 omitted
149
-
150
- # reduce 5 omitted
151
-
152
- # reduce 6 omitted
153
-
154
- def _reduce_7(val, _values)
155
- Group.new(val[1])
156
- end
157
-
158
- def _reduce_8(val, _values)
159
- Or.new([val.first, val.last])
160
- end
161
-
162
- def _reduce_9(val, _values)
163
- Or.new([val.first, val.last])
164
- end
165
-
166
- def _reduce_10(val, _values)
167
- Star.new(Symbol.new(val.last, Symbol::GREEDY_EXP))
7
+ module Journey # :nodoc:
8
+ class Parser # :nodoc:
9
+ include Journey::Nodes
10
+
11
+ def self.parse(string)
12
+ new.parse string
13
+ end
14
+
15
+ def initialize
16
+ @scanner = Scanner.new
17
+ @next_token = nil
18
+ end
19
+
20
+ def parse(string)
21
+ @scanner.scan_setup(string)
22
+ advance_token
23
+ do_parse
24
+ end
25
+
26
+ private
27
+ def advance_token
28
+ @next_token = @scanner.next_token
29
+ end
30
+
31
+ def do_parse
32
+ parse_expressions
33
+ end
34
+
35
+ def parse_expressions
36
+ node = parse_expression
37
+
38
+ while @next_token
39
+ case @next_token
40
+ when :RPAREN
41
+ break
42
+ when :OR
43
+ node = parse_or(node)
44
+ else
45
+ node = Cat.new(node, parse_expressions)
46
+ end
47
+ end
48
+
49
+ node
50
+ end
51
+
52
+ def parse_or(lhs)
53
+ advance_token
54
+ node = parse_expression
55
+ Or.new([lhs, node])
56
+ end
57
+
58
+ def parse_expression
59
+ if @next_token == :STAR
60
+ parse_star
61
+ elsif @next_token == :LPAREN
62
+ parse_group
63
+ else
64
+ parse_terminal
65
+ end
66
+ end
67
+
68
+ def parse_star
69
+ node = Star.new(Symbol.new(@scanner.last_string, Symbol::GREEDY_EXP))
70
+ advance_token
71
+ node
72
+ end
73
+
74
+ def parse_group
75
+ advance_token
76
+ node = parse_expressions
77
+ if @next_token == :RPAREN
78
+ node = Group.new(node)
79
+ advance_token
80
+ node
81
+ else
82
+ raise ArgumentError, "missing right parenthesis."
83
+ end
84
+ end
85
+
86
+ def parse_terminal
87
+ node = case @next_token
88
+ when :SYMBOL
89
+ Symbol.new(@scanner.last_string)
90
+ when :LITERAL
91
+ Literal.new(@scanner.last_literal)
92
+ when :SLASH
93
+ Slash.new("/")
94
+ when :DOT
95
+ Dot.new(".")
96
+ end
97
+
98
+ advance_token
99
+ node
100
+ end
101
+ end
102
+ end
168
103
  end
169
-
170
- # reduce 11 omitted
171
-
172
- # reduce 12 omitted
173
-
174
- # reduce 13 omitted
175
-
176
- # reduce 14 omitted
177
-
178
- def _reduce_15(val, _values)
179
- Slash.new(val.first)
180
- end
181
-
182
- def _reduce_16(val, _values)
183
- Symbol.new(val.first)
184
- end
185
-
186
- def _reduce_17(val, _values)
187
- Literal.new(val.first)
188
- end
189
-
190
- def _reduce_18(val, _values)
191
- Dot.new(val.first)
192
- end
193
-
194
- def _reduce_none(val, _values)
195
- val[0]
196
- end
197
-
198
- end # class Parser
199
- end # module Journey
200
- end # module ActionDispatch
@@ -7,64 +7,62 @@ require "strscan"
7
7
  module ActionDispatch
8
8
  module Journey # :nodoc:
9
9
  class Scanner # :nodoc:
10
+ STATIC_TOKENS = Array.new(150)
11
+ STATIC_TOKENS[".".ord] = :DOT
12
+ STATIC_TOKENS["/".ord] = :SLASH
13
+ STATIC_TOKENS["(".ord] = :LPAREN
14
+ STATIC_TOKENS[")".ord] = :RPAREN
15
+ STATIC_TOKENS["|".ord] = :OR
16
+ STATIC_TOKENS[":".ord] = :SYMBOL
17
+ STATIC_TOKENS["*".ord] = :STAR
18
+ STATIC_TOKENS.freeze
19
+
20
+ class Scanner < StringScanner
21
+ unless method_defined?(:peek_byte) # https://github.com/ruby/strscan/pull/89
22
+ def peek_byte
23
+ string.getbyte(pos)
24
+ end
25
+ end
26
+ end
27
+
10
28
  def initialize
11
- @ss = nil
29
+ @scanner = nil
30
+ @length = nil
12
31
  end
13
32
 
14
33
  def scan_setup(str)
15
- @ss = StringScanner.new(str)
34
+ @scanner = Scanner.new(str)
16
35
  end
17
36
 
18
- def eos?
19
- @ss.eos?
20
- end
37
+ def next_token
38
+ return if @scanner.eos?
21
39
 
22
- def pos
23
- @ss.pos
40
+ until token = scan || @scanner.eos?; end
41
+ token
24
42
  end
25
43
 
26
- def pre_match
27
- @ss.pre_match
44
+ def last_string
45
+ -@scanner.string.byteslice(@scanner.pos - @length, @length)
28
46
  end
29
47
 
30
- def next_token
31
- return if @ss.eos?
32
-
33
- until token = scan || @ss.eos?; end
34
- token
48
+ def last_literal
49
+ last_str = @scanner.string.byteslice(@scanner.pos - @length, @length)
50
+ last_str.tr! "\\", ""
51
+ -last_str
35
52
  end
36
53
 
37
54
  private
38
- # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards see:
39
- # https://bugs.ruby-lang.org/issues/13077
40
- def dedup_scan(regex)
41
- r = @ss.scan(regex)
42
- r ? -r : nil
43
- end
44
-
45
55
  def scan
56
+ next_byte = @scanner.peek_byte
46
57
  case
47
- # /
48
- when @ss.skip(/\//)
49
- [:SLASH, "/"]
50
- when @ss.skip(/\(/)
51
- [:LPAREN, "("]
52
- when @ss.skip(/\)/)
53
- [:RPAREN, ")"]
54
- when @ss.skip(/\|/)
55
- [:OR, "|"]
56
- when @ss.skip(/\./)
57
- [:DOT, "."]
58
- when text = dedup_scan(/:\w+/)
59
- [:SYMBOL, text]
60
- when text = dedup_scan(/\*\w+/)
61
- [:STAR, text]
62
- when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
63
- text.tr! "\\", ""
64
- [:LITERAL, -text]
65
- # any char
66
- when text = dedup_scan(/./)
67
- [:LITERAL, text]
58
+ when (token = STATIC_TOKENS[next_byte])
59
+ @scanner.pos += 1
60
+ @length = @scanner.skip(/\w+/).to_i + 1 if token == :SYMBOL || token == :STAR
61
+ token
62
+ when @length = @scanner.skip(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
63
+ :LITERAL
64
+ when @length = @scanner.skip(/./)
65
+ :LITERAL
68
66
  end
69
67
  end
70
68
  end
@@ -116,13 +116,15 @@ module ActionDispatch
116
116
  # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
117
117
  #
118
118
  # # Sets a signed cookie, which prevents users from tampering with its value.
119
- # # It can be read using the signed method `cookies.signed[:name]`
120
119
  # cookies.signed[:user_id] = current_user.id
120
+ # # It can be read using the signed method.
121
+ # cookies.signed[:user_id] # => 123
121
122
  #
122
123
  # # Sets an encrypted cookie value before sending it to the client which
123
124
  # # prevent users from reading and tampering with its value.
124
- # # It can be read using the encrypted method `cookies.encrypted[:name]`
125
125
  # cookies.encrypted[:discount] = 45
126
+ # # It can be read using the encrypted method.
127
+ # cookies.encrypted[:discount] # => 45
126
128
  #
127
129
  # # Sets a "permanent" cookie (which expires in 20 years from now).
128
130
  # cookies.permanent[:login] = "XJ-122"
@@ -142,17 +142,30 @@ module ActionDispatch
142
142
 
143
143
  message = []
144
144
  message << " "
145
- message << "#{wrapper.exception_class_name} (#{wrapper.message}):"
146
145
  if wrapper.has_cause?
147
- message << "\nCauses:"
146
+ message << "#{wrapper.exception_class_name} (#{wrapper.message})"
148
147
  wrapper.wrapped_causes.each do |wrapped_cause|
149
- message << "#{wrapped_cause.exception_class_name} (#{wrapped_cause.message})"
148
+ message << "Caused by: #{wrapped_cause.exception_class_name} (#{wrapped_cause.message})"
150
149
  end
150
+
151
+ message << "\nInformation for: #{wrapper.exception_class_name} (#{wrapper.message}):"
152
+ else
153
+ message << "#{wrapper.exception_class_name} (#{wrapper.message}):"
151
154
  end
155
+
152
156
  message.concat(wrapper.annotated_source_code)
153
157
  message << " "
154
158
  message.concat(trace)
155
159
 
160
+ if wrapper.has_cause?
161
+ wrapper.wrapped_causes.each do |wrapped_cause|
162
+ message << "\nInformation for cause: #{wrapped_cause.exception_class_name} (#{wrapped_cause.message}):"
163
+ message.concat(wrapped_cause.annotated_source_code)
164
+ message << " "
165
+ message.concat(wrapped_cause.exception_trace)
166
+ end
167
+ end
168
+
156
169
  log_array(logger, message, request)
157
170
  end
158
171
 
@@ -18,8 +18,8 @@ module ActionDispatch
18
18
  # 2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) requires.
19
19
  # Some Rack servers simply drop preceding headers, and only report the value
20
20
  # that was [given in the last
21
- # header](https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-server
22
- # s). If you are behind multiple proxy servers (like NGINX to HAProxy to
21
+ # header](https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers).
22
+ # If you are behind multiple proxy servers (like NGINX to HAProxy to
23
23
  # Unicorn) then you should test your Rack server to make sure your data is good.
24
24
  #
25
25
  # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING. This
@@ -117,10 +117,9 @@ module ActionDispatch
117
117
  # instead, so we check that too.
118
118
  #
119
119
  # As discussed in [this post about Rails IP
120
- # Spoofing](https://web.archive.org/web/20170626095448/https://blog.gingerlime.c
121
- # om/2012/rails-ip-spoofing-vulnerabilities-and-protection/), while the first IP
122
- # in the list is likely to be the "originating" IP, it could also have been set
123
- # by the client maliciously.
120
+ # Spoofing](https://web.archive.org/web/20170626095448/https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/),
121
+ # while the first IP in the list is likely to be the "originating" IP, it
122
+ # could also have been set by the client maliciously.
124
123
  #
125
124
  # In order to find the first address that is (probably) accurate, we take the
126
125
  # list of IPs, remove known and trusted proxies, and then take the last address
@@ -25,11 +25,12 @@ module ActionDispatch
25
25
  def initialize(app, header:)
26
26
  @app = app
27
27
  @header = header
28
+ @env_header = "HTTP_#{header.upcase.tr("-", "_")}"
28
29
  end
29
30
 
30
31
  def call(env)
31
32
  req = ActionDispatch::Request.new env
32
- req.request_id = make_request_id(req.headers[@header])
33
+ req.request_id = make_request_id(req.get_header(@env_header))
33
34
  @app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
34
35
  end
35
36
 
@@ -11,16 +11,26 @@ module ActionDispatch
11
11
  #
12
12
  # 1. **TLS redirect**: Permanently redirects `http://` requests to `https://`
13
13
  # with the same URL host, path, etc. Enabled by default. Set
14
- # `config.ssl_options` to modify the destination URL (e.g. `redirect: {
15
- # host: "secure.widgets.com", port: 8080 }`), or set `redirect: false` to
16
- # disable this feature.
14
+ # `config.ssl_options` to modify the destination URL:
15
+ #
16
+ # config.ssl_options = { redirect: { host: "secure.widgets.com", port: 8080 }`
17
+ #
18
+ # Or set `redirect: false` to disable redirection.
17
19
  #
18
20
  # Requests can opt-out of redirection with `exclude`:
19
21
  #
20
- # config.ssl_options = { redirect: { exclude: -> request { /healthcheck/.match?(request.path) } } }
22
+ # config.ssl_options = { redirect: { exclude: -> request { request.path == "/up" } } }
21
23
  #
22
24
  # Cookies will not be flagged as secure for excluded requests.
23
25
  #
26
+ # When proxying through a load balancer that terminates SSL, the forwarded
27
+ # request will appear as though it's HTTP instead of HTTPS to the application.
28
+ # This makes redirects and cookie security target HTTP instead of HTTPS.
29
+ # To make the server assume that the proxy already terminated SSL, and
30
+ # that the request really is HTTPS, set `config.assume_ssl` to `true`:
31
+ #
32
+ # config.assume_ssl = true
33
+ #
24
34
  # 2. **Secure cookies**: Sets the `secure` flag on cookies to tell browsers
25
35
  # they must not be sent along with `http://` requests. Enabled by default.
26
36
  # Set `config.ssl_options` with `secure_cookies: false` to disable this
@@ -29,6 +29,7 @@ module ActionDispatch
29
29
  config.action_dispatch.request_id_header = ActionDispatch::Constants::X_REQUEST_ID
30
30
  config.action_dispatch.log_rescued_responses = true
31
31
  config.action_dispatch.debug_exception_log_level = :fatal
32
+ config.action_dispatch.strict_freshness = false
32
33
 
33
34
  config.action_dispatch.default_headers = {
34
35
  "X-Frame-Options" => "SAMEORIGIN",
@@ -69,6 +70,7 @@ module ActionDispatch
69
70
 
70
71
  ActionDispatch::Routing::Mapper.route_source_locations = Rails.env.development?
71
72
 
73
+ ActionDispatch::Http::Cache::Request.strict_freshness = app.config.action_dispatch.strict_freshness
72
74
  ActionDispatch.test_app = app
73
75
  end
74
76
  end
@@ -101,7 +101,7 @@ module ActionDispatch
101
101
  { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ }
102
102
  elsif filter[:grep]
103
103
  grep_pattern = Regexp.new(filter[:grep])
104
- path = URI::DEFAULT_PARSER.escape(filter[:grep])
104
+ path = URI::RFC2396_PARSER.escape(filter[:grep])
105
105
  normalized_path = ("/" + path).squeeze("/")
106
106
 
107
107
  {