roda 3.52.0 → 3.55.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ = New Features
2
+
3
+ * You can now override the type attribute for script tags produced
4
+ by the assets plugin, by providing a :type attribute when calling
5
+ the assets method.
6
+
7
+ = Other Improvements
8
+
9
+ * Reloading the render plugin after the additional_view_directories
10
+ plugin no longer removes the additional view directories from
11
+ the allowed paths for templates.
12
+
13
+ * When using Rack 3, Roda will now use an instance of Rack::Headers
14
+ instead of a plain hash for the headers, allowing for compliance
15
+ with the Rack 3 SPEC (which will require lowercase header keys).
16
+
17
+ * The public, multi_public, and sinatra_helpers plugin now use
18
+ Rack::Files instead of Rack::File if available, as Rack::File will
19
+ be deprecated in Rack 3.0.
20
+
21
+ * The json_parser plugin no longer rewinds the request body before
22
+ and after reading it when used with Rack 3.0, as Rack 3.0 has
23
+ dropped the requirement for rewindable input.
24
+
25
+ * The run_handler plugin now closes bodies for upstream 404 responses
26
+ when using the not_found: :pass option.
27
+
28
+ * The chunked plugin no longer uses Transfer-Encoding: chunked by
29
+ default. Requiring the use of Transfer-Encoding: chunked made the
30
+ plugin only work on HTTP 1.1, and not older or newer versions. The
31
+ plugin still allows for streaming template bodies as they are being
32
+ rendered. To get the previous behavior of forcing the use of
33
+ Transfer-Encoding: chunked, you can use the :force_chunked_encoding
34
+ plugin option
35
+
36
+ * Roda now supports testing with Rack::Lint. This found multiple
37
+ violations of the Rack SPEC which are fixed in this version, and
38
+ should ensure that Roda stays in compliance with the Rack SPEC going
39
+ forward.
40
+
41
+ = Backwards Compatibility
42
+
43
+ * Roda will no longer set the Content-Length header for 205 responses
44
+ when using Rack <2.0.2, as doing so violates the Rack SPEC for those
45
+ Rack versions.
46
+
47
+ * The drop_body plugin now drops response bodies for all 1xx responses,
48
+ not just for 100 and 101 responses, in compliance with the Rack SPEC.
@@ -0,0 +1,12 @@
1
+ = New Features
2
+
3
+ * A :forward_response_headers option has been added to the middleware
4
+ plugin, which uses the response headers added by the middleware
5
+ as default response headers even if the middleware does not handle
6
+ the response. Response headers set by the underlying application
7
+ take precedence over response headers set by the middleware.
8
+
9
+ * The render plugin view method now accepts a block and will pass the
10
+ block to the underlying render method call. This is useful for
11
+ rendering a template that yields inside of an existing layout.
12
+ Previously, you had to nest render calls to do that.
@@ -0,0 +1,66 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The additional_view_directories plugin allows for specifying additional view
7
+ # directories to look in for templates. When rendering a template, it will
8
+ # first try the :views directory specified in the render plugin. If the template
9
+ # file to be rendered does not exist in that directory, it will try each additional
10
+ # view directory specified in this plugin, in order, using the path to the first
11
+ # template file that exists in the file system. If no such path is found, it
12
+ # uses the default path specified by the render plugin.
13
+ #
14
+ # Example:
15
+ #
16
+ # plugin :render, :views=>'dir'
17
+ # plugin :additional_view_directories, ['dir1', 'dir2', 'dir3']
18
+ #
19
+ # route do |r|
20
+ # # Will check the following in order, using path for first
21
+ # # template file that exists:
22
+ # # * dir/t.erb
23
+ # # * dir1/t.erb
24
+ # # * dir2/t.erb
25
+ # # * dir3/t.erb
26
+ # render :t
27
+ # end
28
+ module AdditionalViewDirectories
29
+ # Depend on the render plugin, since this plugin only makes
30
+ # sense when the render plugin is used.
31
+ def self.load_dependencies(app, view_dirs)
32
+ app.plugin :render
33
+ end
34
+
35
+ # Set the additional view directories to look in. Each additional view directory
36
+ # is also added as an allowed path.
37
+ def self.configure(app, view_dirs)
38
+ view_dirs = app.opts[:additional_view_directories] = view_dirs.map{|f| app.expand_path(f, nil)}.freeze
39
+ app.plugin :render, :allowed_paths=>(app.opts[:render][:allowed_paths] + view_dirs).uniq.freeze
40
+ end
41
+
42
+ module InstanceMethods
43
+ private
44
+
45
+ # If the template path does not exist, try looking for the template
46
+ # in each of the additional view directories, in order, returning
47
+ # the first path that exists. If no additional directory includes
48
+ # the template, return the original path.
49
+ def template_path(opts)
50
+ orig_path = super
51
+
52
+ unless File.file?(orig_path)
53
+ self.opts[:additional_view_directories].each do |view_dir|
54
+ path = super(opts.merge(:views=>view_dir))
55
+ return path if File.file?(path)
56
+ end
57
+ end
58
+
59
+ orig_path
60
+ end
61
+ end
62
+ end
63
+
64
+ register_plugin(:additional_view_directories, AdditionalViewDirectories)
65
+ end
66
+ end
@@ -708,6 +708,9 @@ class Roda
708
708
  # To return the tags for a specific asset group, use an array for
709
709
  # the type, such as [:css, :frontend].
710
710
  #
711
+ # You can specify custom attributes for the tag by passing a hash
712
+ # as the attrs argument.
713
+ #
711
714
  # When the assets are not compiled, this will result in a separate
712
715
  # tag for each asset file. When the assets are compiled, this will
713
716
  # result in a single tag to the compiled asset file.
@@ -720,13 +723,13 @@ class Roda
720
723
  attrs[:integrity] = "#{algo}-#{h([[hash].pack('H*')].pack('m').tr("\n", ''))}"
721
724
  end
722
725
 
723
- attrs = attrs.map{|k,v| "#{k}=\"#{h(v)}\""}.join(' ')
726
+ attributes = attrs.map{|k,v| "#{k}=\"#{h(v)}\""}.join(' ')
724
727
 
725
728
  if ltype == :js
726
- tag_start = "<script type=\"text/javascript\" #{attrs} src=\""
729
+ tag_start = "<script#{' type="text/javascript"' unless attrs[:type]} #{attributes} src=\""
727
730
  tag_end = "\"></script>"
728
731
  else
729
- tag_start = "<link rel=\"stylesheet\" #{attrs} href=\""
732
+ tag_start = "<link rel=\"stylesheet\" #{attributes} href=\""
730
733
  tag_end = "\" />"
731
734
  end
732
735
 
@@ -3,11 +3,11 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The chunked plugin allows you to stream responses to clients using
7
- # Transfer-Encoding: chunked. This can significantly improve performance
8
- # of page rendering on the client, as it flushes the headers and top part
9
- # of the layout template (generally containing references to the stylesheet
10
- # and javascript assets) before rendering the content template.
6
+ # The chunked plugin allows you to stream rendered views to clients.
7
+ # This can significantly improve performance of page rendering on the
8
+ # client, as it flushes the headers and top part of the layout template
9
+ # (generally containing references to the stylesheet and javascript assets)
10
+ # before rendering the content template.
11
11
  #
12
12
  # This allows the client to fetch the assets while the template is still
13
13
  # being rendered. Additionally, this plugin makes it easy to defer
@@ -17,11 +17,11 @@ class Roda
17
17
  # order to render the content template, such as retrieving values from a
18
18
  # database.
19
19
  #
20
- # There are a couple disadvantages of streaming using chunked encoding.
21
- # First is that the layout must be rendered before the content, so any state
22
- # changes made in your content template will not affect the layout template.
23
- # Second, error handling is reduced, since if an error occurs while
24
- # rendering a template, a successful response code has already been sent.
20
+ # There are a couple disadvantages of streaming. First is that the layout
21
+ # must be rendered before the content, so any state changes made in your
22
+ # content template will not affect the layout template. Second, error
23
+ # handling is reduced, since if an error occurs while rendering a template,
24
+ # a successful response code has already been sent.
25
25
  #
26
26
  # To use chunked encoding for a response, just call the chunked method
27
27
  # instead of view:
@@ -56,7 +56,6 @@ class Roda
56
56
  # end
57
57
  # end
58
58
  #
59
- #
60
59
  # If you want to chunk all responses, pass the :chunk_by_default option
61
60
  # when loading the plugin:
62
61
  #
@@ -121,12 +120,7 @@ class Roda
121
120
  # flush to output the message to the client inside handle_chunk_error.
122
121
  #
123
122
  # In order for chunking to work, you must make sure that no proxies between
124
- # the application and the client buffer responses. Also, this
125
- # plugin only works for HTTP/1.1 requests since Transfer-Encoding: chunked
126
- # is not supported in HTTP/1.0. If an HTTP/1.0 request is submitted, this
127
- # plugin will automatically fallback to the normal template rendering.
128
- # Note that some proxies including nginx default to HTTP/1.0 even if the
129
- # client supports HTTP/1.1. For nginx, set the proxy_http_version to 1.1.
123
+ # the application and the client buffer responses.
130
124
  #
131
125
  # If you are using nginx and have it set to buffer proxy responses by
132
126
  # default, you can turn this off on a per response basis using the
@@ -135,6 +129,15 @@ class Roda
135
129
  #
136
130
  # plugin :chunked, headers: {'X-Accel-Buffering'=>'no'}
137
131
  #
132
+ # By default, this plugin does not use Transfer-Encoding: chunked, it only
133
+ # returns a body that will stream the response in chunks. If you would like
134
+ # to force the use of Transfer-Encoding: chunked, you can use the
135
+ # :force_chunked_encoding plugin option. If using the
136
+ # :force_chunked_encoding plugin option, chunking will only be used for
137
+ # HTTP/1.1 requests since Transfer-Encoding: chunked is only supported
138
+ # in HTTP/1.1 (non-HTTP/1.1 requests will have behavior similar to
139
+ # calling no_chunk!).
140
+ #
138
141
  # The chunked plugin requires the render plugin, and only works for
139
142
  # template engines that store their template output variable in
140
143
  # @_out_buf. Also, it only works if the content template is directly
@@ -155,12 +158,14 @@ class Roda
155
158
  # :headers :: Set default additional headers to use when calling view
156
159
  def self.configure(app, opts=OPTS)
157
160
  app.opts[:chunk_by_default] = opts[:chunk_by_default]
161
+ app.opts[:force_chunked_encoding] = opts[:force_chunked_encoding]
158
162
  if opts[:headers]
159
163
  app.opts[:chunk_headers] = (app.opts[:chunk_headers] || {}).merge(opts[:headers]).freeze
160
164
  end
161
165
  end
162
166
 
163
- # Rack response body instance for chunked responses
167
+ # Rack response body instance for chunked responses using
168
+ # Transfer-Encoding: chunked.
164
169
  class Body
165
170
  # Save the scope of the current request handling.
166
171
  def initialize(scope)
@@ -184,6 +189,22 @@ class Roda
184
189
  end
185
190
  end
186
191
 
192
+ # Rack response body instance for chunked responses not
193
+ # using Transfer-Encoding: chunked.
194
+ class StreamBody
195
+ # Save the scope of the current request handling.
196
+ def initialize(scope)
197
+ @scope = scope
198
+ end
199
+
200
+ # Yield each non-empty chunk as the body.
201
+ def each(&block)
202
+ @scope.each_chunk do |chunk|
203
+ yield chunk if chunk && !chunk.empty?
204
+ end
205
+ end
206
+ end
207
+
187
208
  module InstanceMethods
188
209
  # Disable chunking for the current request. Mostly useful when
189
210
  # chunking is turned on by default.
@@ -194,7 +215,7 @@ class Roda
194
215
  # If chunking by default, call chunked if it hasn't yet been
195
216
  # called and chunking is not specifically disabled.
196
217
  def view(*a)
197
- if opts[:chunk_by_default] && !defined?(@_chunked)
218
+ if opts[:chunk_by_default] && !defined?(@_chunked) && !defined?(yield)
198
219
  chunked(*a)
199
220
  else
200
221
  super
@@ -205,7 +226,7 @@ class Roda
205
226
  # an overview. If a block is given, it is passed to #delay.
206
227
  def chunked(template, opts=OPTS, &block)
207
228
  unless defined?(@_chunked)
208
- @_chunked = env['HTTP_VERSION'] == "HTTP/1.1"
229
+ @_chunked = !self.opts[:force_chunked_encoding] || env['HTTP_VERSION'] == "HTTP/1.1"
209
230
  end
210
231
 
211
232
  if block
@@ -235,9 +256,14 @@ class Roda
235
256
  if chunk_headers = self.opts[:chunk_headers]
236
257
  headers.merge!(chunk_headers)
237
258
  end
238
- headers['Transfer-Encoding'] = 'chunked'
259
+ if self.opts[:force_chunked_encoding]
260
+ headers['Transfer-Encoding'] = 'chunked'
261
+ body = Body.new(self)
262
+ else
263
+ body = StreamBody.new(self)
264
+ end
239
265
 
240
- throw :halt, res.finish_with_body(Body.new(self))
266
+ throw :halt, res.finish_with_body(body)
241
267
  end
242
268
 
243
269
  # Delay the execution of the block until right before the
@@ -15,22 +15,23 @@ class Roda
15
15
  DROP_BODY_STATUSES = [100, 101, 102, 204, 205, 304].freeze
16
16
  RodaPlugins.deprecate_constant(self, :DROP_BODY_STATUSES)
17
17
 
18
+ DROP_BODY_RANGE = 100..199
19
+ private_constant :DROP_BODY_RANGE
20
+
18
21
  # If the response status indicates a body should not be
19
22
  # returned, use an empty body and remove the Content-Length
20
23
  # and Content-Type headers.
21
24
  def finish
22
25
  r = super
23
26
  case r[0]
24
- when 100, 101, 102, 204, 304
27
+ when DROP_BODY_RANGE, 204, 304
25
28
  r[2] = EMPTY_ARRAY
26
29
  h = r[1]
27
30
  h.delete("Content-Length")
28
31
  h.delete("Content-Type")
29
32
  when 205
30
33
  r[2] = EMPTY_ARRAY
31
- h = r[1]
32
- h["Content-Length"] = '0'
33
- h.delete("Content-Type")
34
+ empty_205_headers(r[1])
34
35
  end
35
36
  r
36
37
  end
@@ -14,6 +14,11 @@ class Roda
14
14
  #
15
15
  # plugin :heartbeat, path: '/status'
16
16
  module Heartbeat
17
+ # :nocov:
18
+ HEADER_CLASS = (defined?(Rack::Headers) && Rack::Headers.is_a?(Class)) ? Rack::Headers : Hash
19
+ # :nocov:
20
+ private_constant :HEADER_CLASS
21
+
17
22
  HEARTBEAT_RESPONSE = [200, {'Content-Type'=>'text/plain'}.freeze, ['OK'.freeze].freeze].freeze
18
23
 
19
24
  # Set the heartbeat path to the given path.
@@ -28,7 +33,7 @@ class Roda
28
33
  def _roda_before_20__heartbeat
29
34
  if env['PATH_INFO'] == opts[:heartbeat_path]
30
35
  response = HEARTBEAT_RESPONSE.dup
31
- response[1] = Hash[response[1]]
36
+ response[1] = HEADER_CLASS[response[1]]
32
37
  throw :halt, response
33
38
  end
34
39
  end
@@ -52,17 +52,29 @@ class Roda
52
52
  end
53
53
 
54
54
  class Params < Rack::QueryParser::Params
55
- def initialize(limit = Rack::Utils.key_space_limit)
56
- @limit = limit
57
- @size = 0
58
- @params = Hash.new(&INDIFFERENT_PROC)
55
+ # :nocov:
56
+ if Rack.release >= '2.3'
57
+ def initialize
58
+ @size = 0
59
+ @params = Hash.new(&INDIFFERENT_PROC)
60
+ end
61
+ else
62
+ # :nocov:
63
+ def initialize(limit = Rack::Utils.key_space_limit)
64
+ @limit = limit
65
+ @size = 0
66
+ @params = Hash.new(&INDIFFERENT_PROC)
67
+ end
59
68
  end
60
69
  end
61
70
 
62
71
  end
63
72
 
64
73
  module RequestMethods
65
- QUERY_PARSER = Rack::Utils.default_query_parser = QueryParser.new(QueryParser::Params, 65536, 100)
74
+ # :nocov:
75
+ query_parser = Rack.release >= '2.3' ? QueryParser.new(QueryParser::Params, 32) : QueryParser.new(QueryParser::Params, 65536, 32)
76
+ # :nocov:
77
+ QUERY_PARSER = Rack::Utils.default_query_parser = query_parser
66
78
 
67
79
  private
68
80
 
@@ -52,9 +52,7 @@ class Roda
52
52
  if post_params = (env["roda.json_params"] || env["rack.request.form_hash"])
53
53
  post_params
54
54
  elsif (input = env["rack.input"]) && content_type =~ /json/
55
- input.rewind
56
- str = input.read
57
- input.rewind
55
+ str = _read_json_input(input)
58
56
  return super if str.empty?
59
57
  begin
60
58
  json_params = parse_json(str)
@@ -81,6 +79,23 @@ class Roda
81
79
  args << self if roda_class.opts[:json_parser_include_request]
82
80
  roda_class.opts[:json_parser_parser].call(*args)
83
81
  end
82
+
83
+
84
+ # Rack 3 dropped requirement that input be rewindable
85
+ if Rack.release >= '2.3'
86
+ # :nocov:
87
+ def _read_json_input(input)
88
+ input.read
89
+ end
90
+ # :nocov:
91
+ else
92
+ def _read_json_input(input)
93
+ input.rewind
94
+ str = input.read
95
+ input.rewind
96
+ str
97
+ end
98
+ end
84
99
  end
85
100
  end
86
101
 
@@ -73,11 +73,16 @@ class Roda
73
73
  # and rack response for all requests passing through the middleware,
74
74
  # after either the middleware or next app handles the request
75
75
  # and returns a response.
76
+ # :forward_response_headers :: Whether changes to the response headers made inside
77
+ # the middleware's route block should be applied to the
78
+ # final response when the request is forwarded to the app.
79
+ # Defaults to false.
76
80
  def self.configure(app, opts={}, &block)
77
81
  app.opts[:middleware_env_var] = opts[:env_var] if opts.has_key?(:env_var)
78
82
  app.opts[:middleware_env_var] ||= 'roda.forward_next'
79
83
  app.opts[:middleware_configure] = block if block
80
84
  app.opts[:middleware_handle_result] = opts[:handle_result]
85
+ app.opts[:middleware_forward_response_headers] = opts[:forward_response_headers]
81
86
  end
82
87
 
83
88
  # Forwarder instances are what is actually used as middleware.
@@ -108,6 +113,10 @@ class Roda
108
113
 
109
114
  if call_next
110
115
  res = @app.call(env)
116
+
117
+ if modified_headers = env.delete('roda.response_headers')
118
+ res[1] = modified_headers.merge(res[1])
119
+ end
111
120
  end
112
121
 
113
122
  if handle_result = @mid.opts[:middleware_handle_result]
@@ -135,7 +144,10 @@ class Roda
135
144
  def call(&block)
136
145
  super do |r|
137
146
  res = instance_exec(r, &block) # call Fallback
138
- throw :next, true if r.forward_next
147
+ if r.forward_next
148
+ r.env['roda.response_headers'] = response.headers if opts[:middleware_forward_response_headers]
149
+ throw :next, true
150
+ end
139
151
  res
140
152
  end
141
153
  end
@@ -144,7 +156,10 @@ class Roda
144
156
  # that the next middleware is called.
145
157
  def _roda_run_main_route(r)
146
158
  res = super
147
- throw :next, true if r.forward_next
159
+ if r.forward_next
160
+ r.env['roda.response_headers'] = response.headers if opts[:middleware_forward_response_headers]
161
+ throw :next, true
162
+ end
148
163
  res
149
164
  end
150
165
  end
@@ -58,6 +58,10 @@ class Roda
58
58
  # end
59
59
  # end
60
60
  module MultiPublic
61
+ # :nocov:
62
+ RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
63
+ # :nocov:
64
+
61
65
  def self.load_dependencies(app, _, opts=OPTS)
62
66
  app.plugin(:public, opts)
63
67
  end
@@ -67,7 +71,7 @@ class Roda
67
71
  roots = app.opts[:multi_public_servers] = (app.opts[:multi_public_servers] || {}).dup
68
72
  directories.each do |key, path|
69
73
  path, headers, mime = path
70
- roots[key] = ::Rack::File.new(app.expand_path(path), headers||{}, mime||'text/plain')
74
+ roots[key] = RACK_FILES.new(app.expand_path(path), headers||{}, mime||'text/plain')
71
75
  end
72
76
  roots.freeze
73
77
  end
@@ -37,6 +37,9 @@ class Roda
37
37
  module Public
38
38
  SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact)
39
39
  PARSER = URI::DEFAULT_PARSER
40
+ # :nocov:
41
+ RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
42
+ # :nocov:
40
43
 
41
44
  # Use options given to setup a Rack::File instance for serving files. Options:
42
45
  # :default_mime :: The default mime type to use if the mime type is not recognized.
@@ -52,7 +55,7 @@ class Roda
52
55
  elsif !app.opts[:public_root]
53
56
  app.opts[:public_root] = app.expand_path("public")
54
57
  end
55
- app.opts[:public_server] = ::Rack::File.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
58
+ app.opts[:public_server] = RACK_FILES.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
56
59
  app.opts[:public_gzip] = opts[:gzip]
57
60
  app.opts[:public_brotli] = opts[:brotli]
58
61
  end
@@ -99,7 +102,10 @@ class Roda
99
102
  public_serve_compressed(server, path, '.gz', 'gzip') if roda_opts[:public_gzip]
100
103
 
101
104
  if public_file_readable?(path)
102
- halt public_serve(server, path)
105
+ s, h, b = public_serve(server, path)
106
+ headers = response.headers
107
+ headers.replace(h)
108
+ halt [s, headers, b]
103
109
  end
104
110
  end
105
111
 
@@ -108,21 +114,22 @@ class Roda
108
114
  compressed_path = path + suffix
109
115
 
110
116
  if public_file_readable?(compressed_path)
111
- res = public_serve(server, compressed_path)
112
- headers = res[1]
117
+ s, h, b = public_serve(server, compressed_path)
118
+ headers = response.headers
119
+ headers.replace(h)
113
120
 
114
- unless res[0] == 304
121
+ unless s == 304
115
122
  headers['Content-Type'] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
116
123
  headers['Content-Encoding'] = encoding
117
124
  end
118
125
 
119
- halt res
126
+ halt [s, headers, b]
120
127
  end
121
128
  end
122
129
  end
123
130
 
124
131
  if ::Rack.release > '2'
125
- # Serve the given path using the given Rack::File server.
132
+ # Serve the given path using the given Rack::Files server.
126
133
  def public_serve(server, path)
127
134
  server.serving(self, path)
128
135
  end
@@ -495,8 +495,10 @@ class Roda
495
495
 
496
496
  # Render the given template. If there is a default layout
497
497
  # for the class, take the result of the template rendering
498
- # and render it inside the layout. See Render for details.
499
- def view(template, opts = (content = _optimized_view_content(template); OPTS))
498
+ # and render it inside the layout. Blocks passed to view
499
+ # are passed to render when rendering the template.
500
+ # See Render for details.
501
+ def view(template, opts = (content = _optimized_view_content(template) unless defined?(yield); OPTS), &block)
500
502
  if content
501
503
  # First, check if the optimized layout method has already been created,
502
504
  # and use it if so. This way avoids the extra conditional and local variable
@@ -516,7 +518,7 @@ class Roda
516
518
  end
517
519
  else
518
520
  opts = parse_template_opts(template, opts)
519
- content = opts[:content] || render_template(opts)
521
+ content = opts[:content] || render_template(opts, &block)
520
522
  end
521
523
 
522
524
  if layout_opts = view_layout_opts(opts)
@@ -191,7 +191,9 @@ class Roda
191
191
  when :raise
192
192
  raise InvalidToken, msg
193
193
  when :empty_403
194
- throw :halt, [403, {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, []]
194
+ @_response.status = 403
195
+ @_response.headers.replace('Content-Type'=>'text/html', 'Content-Length'=>'0')
196
+ throw :halt, @_response.finish_with_body([])
195
197
  when :clear_session
196
198
  session.clear
197
199
  when :csrf_failure_method
@@ -35,7 +35,13 @@ class Roda
35
35
  def run(app, opts=OPTS)
36
36
  res = catch(:halt){super(app)}
37
37
  yield res if defined?(yield)
38
- throw(:halt, res) unless opts[:not_found] == :pass && res[0] == 404
38
+ if opts[:not_found] == :pass && res[0] == 404
39
+ body = res[2]
40
+ body.close if body.respond_to?(:close)
41
+ nil
42
+ else
43
+ throw(:halt, res)
44
+ end
39
45
  end
40
46
  end
41
47
  end
@@ -215,6 +215,10 @@ class Roda
215
215
  ISO88591_ENCODING = Encoding.find('ISO-8859-1')
216
216
  BINARY_ENCODING = Encoding.find('BINARY')
217
217
 
218
+ # :nocov:
219
+ RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
220
+ # :nocov:
221
+
218
222
  # Depend on the status_303 plugin.
219
223
  def self.load_dependencies(app, _opts = nil)
220
224
  app.plugin :status_303
@@ -333,7 +337,7 @@ class Roda
333
337
  last_modified(lm)
334
338
  end
335
339
 
336
- file = ::Rack::File.new nil
340
+ file = RACK_FILES.new nil
337
341
  s, h, b = if Rack.release > '2'
338
342
  file.serving(self, path)
339
343
  else