rack 1.4.1 → 1.4.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- data/COPYING +1 -1
- data/KNOWN-ISSUES +9 -0
- data/README.rdoc +72 -7
- data/Rakefile +18 -11
- data/SPEC +3 -1
- data/contrib/rack.png +0 -0
- data/contrib/rack.svg +150 -0
- data/contrib/rdoc.css +412 -0
- data/lib/rack/auth/basic.rb +1 -1
- data/lib/rack/auth/digest/nonce.rb +1 -1
- data/lib/rack/backports/uri/common_18.rb +14 -28
- data/lib/rack/backports/uri/common_192.rb +14 -17
- data/lib/rack/backports/uri/common_193.rb +29 -0
- data/lib/rack/body_proxy.rb +10 -0
- data/lib/rack/builder.rb +1 -1
- data/lib/rack/cascade.rb +11 -0
- data/lib/rack/commonlogger.rb +18 -5
- data/lib/rack/deflater.rb +5 -1
- data/lib/rack/directory.rb +1 -1
- data/lib/rack/etag.rb +6 -3
- data/lib/rack/file.rb +13 -4
- data/lib/rack/head.rb +1 -0
- data/lib/rack/lint.rb +3 -1
- data/lib/rack/lock.rb +3 -4
- data/lib/rack/mime.rb +1 -1
- data/lib/rack/mock.rb +3 -2
- data/lib/rack/multipart.rb +2 -2
- data/lib/rack/multipart/parser.rb +6 -4
- data/lib/rack/reloader.rb +1 -1
- data/lib/rack/request.rb +2 -4
- data/lib/rack/response.rb +2 -1
- data/lib/rack/server.rb +28 -2
- data/lib/rack/session/abstract/id.rb +5 -0
- data/lib/rack/session/cookie.rb +9 -0
- data/lib/rack/static.rb +90 -8
- data/lib/rack/utils.rb +17 -10
- data/rack.gemspec +3 -3
- data/test/builder/line.ru +1 -0
- data/test/cgi/assets/folder/test.js +1 -0
- data/test/cgi/assets/fonts/font.eot +1 -0
- data/test/cgi/assets/images/image.png +1 -0
- data/test/cgi/assets/index.html +1 -0
- data/test/cgi/assets/javascripts/app.js +1 -0
- data/test/cgi/assets/stylesheets/app.css +1 -0
- data/test/spec_auth_basic.rb +8 -0
- data/test/spec_auth_digest.rb +14 -0
- data/test/spec_body_proxy.rb +4 -0
- data/test/spec_builder.rb +7 -1
- data/test/spec_cascade.rb +8 -0
- data/test/spec_chunked.rb +6 -6
- data/test/spec_config.rb +0 -1
- data/test/spec_content_length.rb +26 -13
- data/test/spec_content_type.rb +15 -5
- data/test/spec_deflater.rb +35 -17
- data/test/spec_directory.rb +20 -1
- data/test/spec_etag.rb +29 -13
- data/test/spec_file.rb +42 -25
- data/test/spec_head.rb +25 -7
- data/test/spec_lobster.rb +20 -5
- data/test/spec_lock.rb +46 -21
- data/test/spec_logger.rb +2 -7
- data/test/spec_methodoverride.rb +21 -22
- data/test/spec_mock.rb +12 -7
- data/test/spec_multipart.rb +29 -0
- data/test/spec_nulllogger.rb +13 -2
- data/test/spec_recursive.rb +12 -9
- data/test/spec_request.rb +2 -2
- data/test/spec_response.rb +30 -0
- data/test/spec_runtime.rb +15 -5
- data/test/spec_sendfile.rb +11 -8
- data/test/spec_server.rb +47 -0
- data/test/spec_session_cookie.rb +68 -1
- data/test/spec_session_memcache.rb +10 -8
- data/test/spec_session_pool.rb +13 -10
- data/test/spec_showexceptions.rb +9 -4
- data/test/spec_showstatus.rb +10 -5
- data/test/spec_static.rb +85 -9
- data/test/spec_urlmap.rb +10 -10
- data/test/spec_utils.rb +14 -1
- data/test/static/another/index.html +1 -0
- metadata +21 -8
data/lib/rack/reloader.rb
CHANGED
data/lib/rack/request.rb
CHANGED
@@ -260,12 +260,10 @@ module Rack
|
|
260
260
|
# the Cookie header such that those with more specific Path attributes
|
261
261
|
# precede those with less specific. Ordering with respect to other
|
262
262
|
# attributes (e.g., Domain) is unspecified.
|
263
|
-
Utils.parse_query(string, ';,')
|
263
|
+
cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
|
264
|
+
cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
|
264
265
|
@env["rack.request.cookie_string"] = string
|
265
266
|
hash
|
266
|
-
rescue => error
|
267
|
-
error.message.replace "cannot parse Cookie header: #{error.message}"
|
268
|
-
raise
|
269
267
|
end
|
270
268
|
|
271
269
|
def xhr?
|
data/lib/rack/response.rb
CHANGED
@@ -74,9 +74,10 @@ module Rack
|
|
74
74
|
if [204, 205, 304].include?(status.to_i)
|
75
75
|
header.delete "Content-Type"
|
76
76
|
header.delete "Content-Length"
|
77
|
+
close
|
77
78
|
[status.to_i, header, []]
|
78
79
|
else
|
79
|
-
[status.to_i, header, self]
|
80
|
+
[status.to_i, header, BodyProxy.new(self){}]
|
80
81
|
end
|
81
82
|
end
|
82
83
|
alias to_a finish # For *response
|
data/lib/rack/server.rb
CHANGED
@@ -26,7 +26,7 @@ module Rack
|
|
26
26
|
|
27
27
|
opts.on("-I", "--include PATH",
|
28
28
|
"specify $LOAD_PATH (may be used more than once)") { |path|
|
29
|
-
options[:include]
|
29
|
+
(options[:include] ||= []).concat(path.split(":"))
|
30
30
|
}
|
31
31
|
|
32
32
|
opts.on("-r", "--require LIBRARY",
|
@@ -247,11 +247,14 @@ module Rack
|
|
247
247
|
pp app
|
248
248
|
end
|
249
249
|
|
250
|
+
check_pid! if options[:pid]
|
251
|
+
|
250
252
|
# Touch the wrapped app, so that the config.ru is loaded before
|
251
253
|
# daemonization (i.e. before chdir, etc).
|
252
254
|
wrapped_app
|
253
255
|
|
254
256
|
daemonize_app if options[:daemonize]
|
257
|
+
|
255
258
|
write_pid if options[:pid]
|
256
259
|
|
257
260
|
trap(:INT) do
|
@@ -274,7 +277,7 @@ module Rack
|
|
274
277
|
options = default_options
|
275
278
|
|
276
279
|
# Don't evaluate CGI ISINDEX parameters.
|
277
|
-
# http://
|
280
|
+
# http://www.meb.uni-bonn.de/docs/cgi/cl.html
|
278
281
|
args.clear if ENV.include?("REQUEST_METHOD")
|
279
282
|
|
280
283
|
options.merge! opt_parser.parse!(args)
|
@@ -319,5 +322,28 @@ module Rack
|
|
319
322
|
::File.open(options[:pid], 'w'){ |f| f.write("#{Process.pid}") }
|
320
323
|
at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
|
321
324
|
end
|
325
|
+
|
326
|
+
def check_pid!
|
327
|
+
case pidfile_process_status
|
328
|
+
when :running, :not_owned
|
329
|
+
$stderr.puts "A server is already running. Check #{options[:pid]}."
|
330
|
+
exit(1)
|
331
|
+
when :dead
|
332
|
+
::File.delete(options[:pid])
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def pidfile_process_status
|
337
|
+
return :exited unless ::File.exist?(options[:pid])
|
338
|
+
|
339
|
+
pid = ::File.read(options[:pid]).to_i
|
340
|
+
Process.kill(0, pid)
|
341
|
+
:running
|
342
|
+
rescue Errno::ESRCH
|
343
|
+
:dead
|
344
|
+
rescue Errno::EPERM
|
345
|
+
:not_owned
|
346
|
+
end
|
347
|
+
|
322
348
|
end
|
323
349
|
end
|
data/lib/rack/session/cookie.rb
CHANGED
@@ -82,6 +82,15 @@ module Rack
|
|
82
82
|
|
83
83
|
def initialize(app, options={})
|
84
84
|
@secrets = options.values_at(:secret, :old_secret).compact
|
85
|
+
warn <<-MSG unless @secrets.size >= 1
|
86
|
+
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
|
87
|
+
This poses a security threat. It is strongly recommended that you
|
88
|
+
provide a secret to prevent exploits that may be possible from crafted
|
89
|
+
cookies. This will not be supported in future versions of Rack, and
|
90
|
+
future versions will even invalidate your existing user cookies.
|
91
|
+
|
92
|
+
Called from: #{caller[0]}.
|
93
|
+
MSG
|
85
94
|
@coder = options[:coder] ||= Base64::Marshal.new
|
86
95
|
super(app, options.merge!(:cookie_only => true))
|
87
96
|
end
|
data/lib/rack/static.rb
CHANGED
@@ -26,13 +26,58 @@ module Rack
|
|
26
26
|
# directory but uses index.html as default route for "/"
|
27
27
|
#
|
28
28
|
# use Rack::Static, :urls => [""], :root => 'public', :index =>
|
29
|
-
# '
|
29
|
+
# 'index.html'
|
30
30
|
#
|
31
|
-
# Set
|
31
|
+
# Set custom HTTP Headers for based on rules:
|
32
32
|
#
|
33
|
-
# use Rack::Static, :root => 'public',
|
33
|
+
# use Rack::Static, :root => 'public',
|
34
|
+
# :header_rules => [
|
35
|
+
# [rule, {header_field => content, header_field => content}],
|
36
|
+
# [rule, {header_field => content}]
|
37
|
+
# ]
|
38
|
+
#
|
39
|
+
# Rules for selecting files:
|
40
|
+
#
|
41
|
+
# 1) All files
|
42
|
+
# Provide the :all symbol
|
43
|
+
# :all => Matches every file
|
44
|
+
#
|
45
|
+
# 2) Folders
|
46
|
+
# Provide the folder path as a string
|
47
|
+
# '/folder' or '/folder/subfolder' => Matches files in a certain folder
|
48
|
+
#
|
49
|
+
# 3) File Extensions
|
50
|
+
# Provide the file extensions as an array
|
51
|
+
# ['css', 'js'] or %w(css js) => Matches files ending in .css or .js
|
52
|
+
#
|
53
|
+
# 4) Regular Expressions / Regexp
|
54
|
+
# Provide a regular expression
|
55
|
+
# %r{\.(?:css|js)\z} => Matches files ending in .css or .js
|
56
|
+
# /\.(?:eot|ttf|otf|woff|svg)\z/ => Matches files ending in
|
57
|
+
# the most common web font formats (.eot, .ttf, .otf, .woff, .svg)
|
58
|
+
# Note: This Regexp is available as a shortcut, using the :fonts rule
|
59
|
+
#
|
60
|
+
# 5) Font Shortcut
|
61
|
+
# Provide the :fonts symbol
|
62
|
+
# :fonts => Uses the Regexp rule stated right above to match all common web font endings
|
63
|
+
#
|
64
|
+
# Rule Ordering:
|
65
|
+
# Rules are applied in the order that they are provided.
|
66
|
+
# List rather general rules above special ones.
|
67
|
+
#
|
68
|
+
# Complete example use case including HTTP header rules:
|
69
|
+
#
|
70
|
+
# use Rack::Static, :root => 'public',
|
71
|
+
# :header_rules => [
|
72
|
+
# # Cache all static files in public caches (e.g. Rack::Cache)
|
73
|
+
# # as well as in the browser
|
74
|
+
# [:all, {'Cache-Control' => 'public, max-age=31536000'}],
|
75
|
+
#
|
76
|
+
# # Provide web fonts with cross-origin access-control-headers
|
77
|
+
# # Firefox requires this when serving assets using a Content Delivery Network
|
78
|
+
# [:fonts, {'Access-Control-Allow-Origin' => '*'}]
|
79
|
+
# ]
|
34
80
|
#
|
35
|
-
|
36
81
|
class Static
|
37
82
|
|
38
83
|
def initialize(app, options={})
|
@@ -40,12 +85,18 @@ module Rack
|
|
40
85
|
@urls = options[:urls] || ["/favicon.ico"]
|
41
86
|
@index = options[:index]
|
42
87
|
root = options[:root] || Dir.pwd
|
43
|
-
|
44
|
-
|
88
|
+
|
89
|
+
# HTTP Headers
|
90
|
+
@header_rules = options[:header_rules] || []
|
91
|
+
# Allow for legacy :cache_control option while prioritizing global header_rules setting
|
92
|
+
@header_rules.insert(0, [:all, {'Cache-Control' => options[:cache_control]}]) if options[:cache_control]
|
93
|
+
@headers = {}
|
94
|
+
|
95
|
+
@file_server = Rack::File.new(root, @headers)
|
45
96
|
end
|
46
97
|
|
47
98
|
def overwrite_file_path(path)
|
48
|
-
@urls.kind_of?(Hash) && @urls.key?(path) || @index && path
|
99
|
+
@urls.kind_of?(Hash) && @urls.key?(path) || @index && path =~ /\/$/
|
49
100
|
end
|
50
101
|
|
51
102
|
def route_file(path)
|
@@ -60,12 +111,43 @@ module Rack
|
|
60
111
|
path = env["PATH_INFO"]
|
61
112
|
|
62
113
|
if can_serve(path)
|
63
|
-
env["PATH_INFO"] = (path
|
114
|
+
env["PATH_INFO"] = (path =~ /\/$/ ? path + @index : @urls[path]) if overwrite_file_path(path)
|
115
|
+
@path = env["PATH_INFO"]
|
116
|
+
apply_header_rules
|
64
117
|
@file_server.call(env)
|
65
118
|
else
|
66
119
|
@app.call(env)
|
67
120
|
end
|
68
121
|
end
|
69
122
|
|
123
|
+
# Convert HTTP header rules to HTTP headers
|
124
|
+
def apply_header_rules
|
125
|
+
@header_rules.each do |rule, headers|
|
126
|
+
apply_rule(rule, headers)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def apply_rule(rule, headers)
|
131
|
+
case rule
|
132
|
+
when :all # All files
|
133
|
+
set_headers(headers)
|
134
|
+
when :fonts # Fonts Shortcut
|
135
|
+
set_headers(headers) if @path.match(/\.(?:ttf|otf|eot|woff|svg)\z/)
|
136
|
+
when String # Folder
|
137
|
+
path = ::Rack::Utils.unescape(@path)
|
138
|
+
set_headers(headers) if (path.start_with?(rule) || path.start_with?('/' + rule))
|
139
|
+
when Array # Extension/Extensions
|
140
|
+
extensions = rule.join('|')
|
141
|
+
set_headers(headers) if @path.match(/\.(#{extensions})\z/)
|
142
|
+
when Regexp # Flexible Regexp
|
143
|
+
set_headers(headers) if @path.match(rule)
|
144
|
+
else
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def set_headers(headers)
|
149
|
+
headers.each { |field, content| @headers[field] = content }
|
150
|
+
end
|
151
|
+
|
70
152
|
end
|
71
153
|
end
|
data/lib/rack/utils.rb
CHANGED
@@ -5,11 +5,14 @@ require 'tempfile'
|
|
5
5
|
require 'rack/multipart'
|
6
6
|
|
7
7
|
major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
|
8
|
+
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
|
8
9
|
|
9
10
|
if major == 1 && minor < 9
|
10
11
|
require 'rack/backports/uri/common_18'
|
11
|
-
elsif major == 1 && minor == 9 && patch
|
12
|
+
elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 320 && RUBY_ENGINE != 'jruby'
|
12
13
|
require 'rack/backports/uri/common_192'
|
14
|
+
elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
|
15
|
+
require 'rack/backports/uri/common_193'
|
13
16
|
else
|
14
17
|
require 'uri/common'
|
15
18
|
end
|
@@ -60,11 +63,15 @@ module Rack
|
|
60
63
|
# and ';' characters. You can also use this to parse
|
61
64
|
# cookies by changing the characters used in the second
|
62
65
|
# parameter (which defaults to '&;').
|
63
|
-
def parse_query(qs, d = nil)
|
66
|
+
def parse_query(qs, d = nil, &unescaper)
|
67
|
+
unescaper ||= method(:unescape)
|
68
|
+
|
64
69
|
params = KeySpaceConstrainedParams.new
|
65
70
|
|
66
71
|
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
67
|
-
|
72
|
+
next if p.empty?
|
73
|
+
k, v = p.split('=', 2).map(&unescaper)
|
74
|
+
next unless k || v
|
68
75
|
|
69
76
|
if cur = params[k]
|
70
77
|
if cur.class == Array
|
@@ -309,16 +316,16 @@ module Rack
|
|
309
316
|
def byte_ranges(env, size)
|
310
317
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
311
318
|
http_range = env['HTTP_RANGE']
|
312
|
-
return nil unless http_range
|
319
|
+
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
313
320
|
ranges = []
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
r0,r1 = matches[1], matches[2]
|
321
|
+
$1.split(/,\s*/).each do |range_spec|
|
322
|
+
return nil unless range_spec =~ /(\d*)-(\d*)/
|
323
|
+
r0,r1 = $1, $2
|
318
324
|
if r0.empty?
|
319
325
|
return nil if r1.empty?
|
320
326
|
# suffix-byte-range-spec, represents trailing suffix of file
|
321
|
-
r0 =
|
327
|
+
r0 = size - r1.to_i
|
328
|
+
r0 = 0 if r0 < 0
|
322
329
|
r1 = size - 1
|
323
330
|
else
|
324
331
|
r0 = r0.to_i
|
@@ -442,7 +449,7 @@ module Rack
|
|
442
449
|
end
|
443
450
|
|
444
451
|
def []=(key, value)
|
445
|
-
@size += key.size
|
452
|
+
@size += key.size if key && !@params.key?(key)
|
446
453
|
raise RangeError, 'exceeded available parameter key space' if @size > @limit
|
447
454
|
@params[key] = value
|
448
455
|
end
|
data/rack.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "rack"
|
3
|
-
s.version = "1.4.
|
3
|
+
s.version = "1.4.2"
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
s.summary = "a modular Ruby webserver interface"
|
6
6
|
|
@@ -11,7 +11,7 @@ the simplest way possible, it unifies and distills the API for web
|
|
11
11
|
servers, web frameworks, and software in between (the so-called
|
12
12
|
middleware) into a single method call.
|
13
13
|
|
14
|
-
Also see http://rack.
|
14
|
+
Also see http://rack.github.com/.
|
15
15
|
EOF
|
16
16
|
|
17
17
|
s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] +
|
@@ -24,7 +24,7 @@ EOF
|
|
24
24
|
|
25
25
|
s.author = 'Christian Neukirchen'
|
26
26
|
s.email = 'chneukirchen@gmail.com'
|
27
|
-
s.homepage = 'http://rack.
|
27
|
+
s.homepage = 'http://rack.github.com/'
|
28
28
|
s.rubyforge_project = 'rack'
|
29
29
|
|
30
30
|
s.add_development_dependency 'bacon'
|
@@ -0,0 +1 @@
|
|
1
|
+
run lambda{ |env| [200, {'Content-Type' => 'text/plain'}, [__LINE__.to_s]] }
|
@@ -0,0 +1 @@
|
|
1
|
+
### TestFile ###
|
@@ -0,0 +1 @@
|
|
1
|
+
### TestFile ###
|
@@ -0,0 +1 @@
|
|
1
|
+
### TestFile ###
|
@@ -0,0 +1 @@
|
|
1
|
+
### TestFile ###
|
@@ -0,0 +1 @@
|
|
1
|
+
### TestFile ###
|
@@ -0,0 +1 @@
|
|
1
|
+
### TestFile ###
|
data/test/spec_auth_basic.rb
CHANGED
@@ -66,6 +66,14 @@ describe Rack::Auth::Basic do
|
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
+
should 'return 400 Bad Request for a malformed authorization header' do
|
70
|
+
request 'HTTP_AUTHORIZATION' => '' do |response|
|
71
|
+
response.should.be.a.client_error
|
72
|
+
response.status.should.equal 400
|
73
|
+
response.should.not.include 'WWW-Authenticate'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
69
77
|
it 'takes realm as optional constructor arg' do
|
70
78
|
app = Rack::Auth::Basic.new(unprotected_app, realm) { true }
|
71
79
|
realm.should == app.realm
|
data/test/spec_auth_digest.rb
CHANGED
@@ -153,6 +153,20 @@ describe Rack::Auth::Digest::MD5 do
|
|
153
153
|
end
|
154
154
|
end
|
155
155
|
|
156
|
+
should 'not rechallenge if nonce is not stale' do
|
157
|
+
begin
|
158
|
+
Rack::Auth::Digest::Nonce.time_limit = 10
|
159
|
+
|
160
|
+
request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', :wait => 1 do |response|
|
161
|
+
response.status.should.equal 200
|
162
|
+
response.body.to_s.should.equal 'Hi Alice'
|
163
|
+
response.headers['WWW-Authenticate'].should.not =~ /\bstale=true\b/
|
164
|
+
end
|
165
|
+
ensure
|
166
|
+
Rack::Auth::Digest::Nonce.time_limit = nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
156
170
|
should 'rechallenge with stale parameter if nonce is stale' do
|
157
171
|
begin
|
158
172
|
Rack::Auth::Digest::Nonce.time_limit = 1
|
data/test/spec_body_proxy.rb
CHANGED
data/test/spec_builder.rb
CHANGED
@@ -193,9 +193,15 @@ describe Rack::Builder do
|
|
193
193
|
|
194
194
|
it "requires anything not ending in .ru" do
|
195
195
|
$: << File.dirname(__FILE__)
|
196
|
-
app,
|
196
|
+
app, * = Rack::Builder.parse_file 'builder/anything'
|
197
197
|
Rack::MockRequest.new(app).get("/").body.to_s.should.equal 'OK'
|
198
198
|
$:.pop
|
199
199
|
end
|
200
|
+
|
201
|
+
it "sets __LINE__ correctly" do
|
202
|
+
app, options = Rack::Builder.parse_file config_file('line.ru')
|
203
|
+
options = nil # ignored, prevents warning
|
204
|
+
Rack::MockRequest.new(app).get("/").body.to_s.should.equal '1'
|
205
|
+
end
|
200
206
|
end
|
201
207
|
end
|
data/test/spec_cascade.rb
CHANGED
@@ -50,4 +50,12 @@ describe Rack::Cascade do
|
|
50
50
|
cascade << app3
|
51
51
|
Rack::MockRequest.new(cascade).get('/foo').should.be.ok
|
52
52
|
end
|
53
|
+
|
54
|
+
should "close the body on cascade" do
|
55
|
+
body = StringIO.new
|
56
|
+
closer = lambda { |env| [404, {}, body] }
|
57
|
+
cascade = Rack::Cascade.new([closer, app3], [404])
|
58
|
+
Rack::MockRequest.new(cascade).get("/foo").should.be.ok
|
59
|
+
body.should.be.closed
|
60
|
+
end
|
53
61
|
end
|