roda 3.53.0 → 3.56.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,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