roda 3.53.0 → 3.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,33 @@
1
+ = New Features
2
+
3
+ * RodaRequest#http_version has been added for determining the HTTP
4
+ version the request was submitted with. This will be a string
5
+ such as "HTTP/1.0", "HTTP/1.1", "HTTP/2", etc. This will use the
6
+ SERVER_PROTOCOL and HTTP_VERSION entries from the environment to
7
+ determine which HTTP version is in use.
8
+
9
+ * The status_handler method in the status_handler plugin now supports
10
+ a :keep_headers option. The value for this option should be an
11
+ array of header names to keep. All other headers are removed. The
12
+ default behavior without the option is still to remove all headers.
13
+
14
+ * A run_require_slash plugin has been added, which will skip
15
+ dispatching to another rack application if the remaining path is not
16
+ empty and does not start with a slash.
17
+
18
+ = Other Improvements
19
+
20
+ * The status_303 plugin will use 303 as the default redirect status
21
+ for non-GET requests for HTTP/2 and higher HTTP versions. Previously,
22
+ it only used 303 for HTTP/1.1.
23
+
24
+ * The not_allowed plugin now overrides the r.root method to return
25
+ 405 responses to non-GET requests to the root.
26
+
27
+ * The not_allowed plugin no longer sets the body when returning 405
28
+ responses using methods such as r.get and r.post. Previously, the
29
+ body was unintentionally set to the same value as the Allow header.
30
+
31
+ * When using the Rack master branch (what will become Rack 3), Roda
32
+ only requires the parts of rack that it uses, instead of requiring
33
+ rack and relying on autoload to load the parts of rack in use.
@@ -36,9 +36,7 @@ class Roda
36
36
  # is also added as an allowed path.
37
37
  def self.configure(app, view_dirs)
38
38
  view_dirs = app.opts[:additional_view_directories] = view_dirs.map{|f| app.expand_path(f, nil)}.freeze
39
- opts = app.opts[:render]
40
- app.opts[:render] = opts.merge(:allowed_paths=>(opts[:allowed_paths] + view_dirs).uniq.freeze)
41
- opts.freeze
39
+ app.plugin :render, :allowed_paths=>(app.opts[:render][:allowed_paths] + view_dirs).uniq.freeze
42
40
  end
43
41
 
44
42
  module InstanceMethods
@@ -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] || @_request.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
@@ -53,7 +53,7 @@ class Roda
53
53
 
54
54
  env = @_request.env
55
55
 
56
- opts[:common_logger_meth].call("#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{env["HTTP_VERSION"]}\" #{result[0]} #{((length = result[1]['Content-Length']) && (length unless length == '0')) || '-'} #{elapsed_time}\n")
56
+ opts[:common_logger_meth].call("#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{result[0]} #{((length = result[1]['Content-Length']) && (length unless length == '0')) || '-'} #{elapsed_time}\n")
57
57
  end
58
58
 
59
59
  # Create timer instance used for timing
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require 'rack/utils'
4
+
3
5
  #
4
6
  class Roda
5
7
  module RodaPlugins
@@ -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
@@ -4,12 +4,16 @@ require 'json'
4
4
 
5
5
  class Roda
6
6
  module RodaPlugins
7
- # The json_parser plugin parses request bodies in json format
7
+ # The json_parser plugin parses request bodies in JSON format
8
8
  # if the request's content type specifies json. This is mostly
9
9
  # designed for use with JSON API sites.
10
10
  #
11
11
  # This only parses the request body as JSON if the Content-Type
12
12
  # header for the request includes "json".
13
+ #
14
+ # The parsed JSON body will be available in +r.POST+, just as a
15
+ # parsed HTML form body would be. It will also be available in
16
+ # +r.params+ (which merges +r.GET+ with +r.POST+).
13
17
  module JsonParser
14
18
  DEFAULT_ERROR_HANDLER = proc{|r| r.halt [400, {}, []]}
15
19
 
@@ -25,7 +29,7 @@ class Roda
25
29
  # object as the second argument, so the parser needs
26
30
  # to respond to +call(str, request)+.
27
31
  # :wrap :: Whether to wrap uploaded JSON data in a hash with a "_json"
28
- # key. Without this, calls to r.params will fail if a non-Hash
32
+ # key. Without this, calls to +r.params+ will fail if a non-Hash
29
33
  # (such as an array) is uploaded in JSON format. A value of
30
34
  # :always will wrap all values, and a value of :unless_hash will
31
35
  # only wrap values that are not already hashes.
@@ -52,9 +56,7 @@ class Roda
52
56
  if post_params = (env["roda.json_params"] || env["rack.request.form_hash"])
53
57
  post_params
54
58
  elsif (input = env["rack.input"]) && content_type =~ /json/
55
- input.rewind
56
- str = input.read
57
- input.rewind
59
+ str = _read_json_input(input)
58
60
  return super if str.empty?
59
61
  begin
60
62
  json_params = parse_json(str)
@@ -81,6 +83,23 @@ class Roda
81
83
  args << self if roda_class.opts[:json_parser_include_request]
82
84
  roda_class.opts[:json_parser_parser].call(*args)
83
85
  end
86
+
87
+
88
+ # Rack 3 dropped requirement that input be rewindable
89
+ if Rack.release >= '2.3'
90
+ # :nocov:
91
+ def _read_json_input(input)
92
+ input.read
93
+ end
94
+ # :nocov:
95
+ else
96
+ def _read_json_input(input)
97
+ input.rewind
98
+ str = input.read
99
+ input.rewind
100
+ str
101
+ end
102
+ end
84
103
  end
85
104
  end
86
105
 
@@ -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
@@ -1,5 +1,13 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ begin
4
+ require 'rack/files'
5
+ rescue LoadError
6
+ # :nocov:
7
+ require 'rack/file'
8
+ # :nocov:
9
+ end
10
+
3
11
  #
4
12
  class Roda
5
13
  module RodaPlugins
@@ -58,6 +66,10 @@ class Roda
58
66
  # end
59
67
  # end
60
68
  module MultiPublic
69
+ # :nocov:
70
+ RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
71
+ # :nocov:
72
+
61
73
  def self.load_dependencies(app, _, opts=OPTS)
62
74
  app.plugin(:public, opts)
63
75
  end
@@ -67,7 +79,7 @@ class Roda
67
79
  roots = app.opts[:multi_public_servers] = (app.opts[:multi_public_servers] || {}).dup
68
80
  directories.each do |key, path|
69
81
  path, headers, mime = path
70
- roots[key] = ::Rack::File.new(app.expand_path(path), headers||{}, mime||'text/plain')
82
+ roots[key] = RACK_FILES.new(app.expand_path(path), headers||{}, mime||'text/plain')
71
83
  end
72
84
  roots.freeze
73
85
  end
@@ -17,6 +17,9 @@ class Roda
17
17
  # will return a 200 response for <tt>GET /</tt> and a 405
18
18
  # response for <tt>POST /</tt>.
19
19
  #
20
+ # This plugin changes the +r.root+ method to return a 405 status
21
+ # for non-GET requests to +/+.
22
+ #
20
23
  # This plugin also changes the +r.is+ method so that if you use
21
24
  # a verb method inside +r.is+, it returns a 405 status if none
22
25
  # of the verb methods match. So this code:
@@ -100,6 +103,15 @@ class Roda
100
103
  end
101
104
  end
102
105
 
106
+ # Treat +r.root+ similar to <tt>r.get ''</tt>, using a 405
107
+ # response for non-GET requests.
108
+ def root
109
+ super
110
+ if @remaining_path == "/" && !is_get?
111
+ always{method_not_allowed("GET")}
112
+ end
113
+ end
114
+
103
115
  # Setup methods for all verbs. If inside an is block and not given
104
116
  # arguments, record the verb used. If given an argument, add an is
105
117
  # check with the arguments.
@@ -129,6 +141,7 @@ class Roda
129
141
  res = response
130
142
  res.status = 405
131
143
  res['Allow'] = verbs
144
+ nil
132
145
  end
133
146
  end
134
147
  end
@@ -2,6 +2,14 @@
2
2
 
3
3
  require 'uri'
4
4
 
5
+ begin
6
+ require 'rack/files'
7
+ rescue LoadError
8
+ # :nocov:
9
+ require 'rack/file'
10
+ # :nocov:
11
+ end
12
+
5
13
  #
6
14
  class Roda
7
15
  module RodaPlugins
@@ -37,6 +45,9 @@ class Roda
37
45
  module Public
38
46
  SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact)
39
47
  PARSER = URI::DEFAULT_PARSER
48
+ # :nocov:
49
+ RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
50
+ # :nocov:
40
51
 
41
52
  # Use options given to setup a Rack::File instance for serving files. Options:
42
53
  # :default_mime :: The default mime type to use if the mime type is not recognized.
@@ -52,7 +63,7 @@ class Roda
52
63
  elsif !app.opts[:public_root]
53
64
  app.opts[:public_root] = app.expand_path("public")
54
65
  end
55
- app.opts[:public_server] = ::Rack::File.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
66
+ app.opts[:public_server] = RACK_FILES.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
56
67
  app.opts[:public_gzip] = opts[:gzip]
57
68
  app.opts[:public_brotli] = opts[:brotli]
58
69
  end
@@ -99,7 +110,10 @@ class Roda
99
110
  public_serve_compressed(server, path, '.gz', 'gzip') if roda_opts[:public_gzip]
100
111
 
101
112
  if public_file_readable?(path)
102
- halt public_serve(server, path)
113
+ s, h, b = public_serve(server, path)
114
+ headers = response.headers
115
+ headers.replace(h)
116
+ halt [s, headers, b]
103
117
  end
104
118
  end
105
119
 
@@ -108,21 +122,22 @@ class Roda
108
122
  compressed_path = path + suffix
109
123
 
110
124
  if public_file_readable?(compressed_path)
111
- res = public_serve(server, compressed_path)
112
- headers = res[1]
125
+ s, h, b = public_serve(server, compressed_path)
126
+ headers = response.headers
127
+ headers.replace(h)
113
128
 
114
- unless res[0] == 304
129
+ unless s == 304
115
130
  headers['Content-Type'] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
116
131
  headers['Content-Encoding'] = encoding
117
132
  end
118
133
 
119
- halt res
134
+ halt [s, headers, b]
120
135
  end
121
136
  end
122
137
  end
123
138
 
124
139
  if ::Rack.release > '2'
125
- # Serve the given path using the given Rack::File server.
140
+ # Serve the given path using the given Rack::Files server.
126
141
  def public_serve(server, path)
127
142
  server.serving(self, path)
128
143
  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)
@@ -4,6 +4,7 @@ require 'base64'
4
4
  require 'openssl'
5
5
  require 'securerandom'
6
6
  require 'uri'
7
+ require 'rack/utils'
7
8
 
8
9
  class Roda
9
10
  module RodaPlugins
@@ -191,7 +192,9 @@ class Roda
191
192
  when :raise
192
193
  raise InvalidToken, msg
193
194
  when :empty_403
194
- throw :halt, [403, {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, []]
195
+ @_response.status = 403
196
+ @_response.headers.replace('Content-Type'=>'text/html', 'Content-Length'=>'0')
197
+ throw :halt, @_response.finish_with_body([])
195
198
  when :clear_session
196
199
  session.clear
197
200
  when :csrf_failure_method
@@ -34,7 +34,7 @@ class Roda
34
34
  # path internally, or a redirect is issued when configured with
35
35
  # <tt>use_redirects: true</tt>.
36
36
  def run(*)
37
- if remaining_path.empty?
37
+ if @remaining_path.empty?
38
38
  if scope.opts[:run_append_slash_redirect]
39
39
  redirect("#{path}/")
40
40
  else