roda 3.10.0 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fa178463d549cd2a8826a8601eccc793e2a3b5a6bb34293ce8511fb8d357128
4
- data.tar.gz: 5b099180432b7efc3bc4839883367a454c876d9a2e6809f533f970e83bd32d0d
3
+ metadata.gz: b7fc4e03ddcb9b4e5fa37af6c0a151aef49965ef251d8ab177e7b530ec034529
4
+ data.tar.gz: 78ab54340363d9b963496e143809ca9f809c619d25f98faa08b08f9e3790ae19
5
5
  SHA512:
6
- metadata.gz: e7ec971ff829c784c21c9660b7964f79755841501449c6372260510a367482a9dde5131bf6b488eadefdd24403f7a19576c8d1176d8d8306fecc861388fb9f7f
7
- data.tar.gz: 7fe25ba12cceccf1b59d9993d4972c0ffb2df2b78b68716f7708ecc58d131bc0858d9b7a1b87ac34bb88a05b57aeb6f0fb68beac648bbdfe2f7c0c3c6933def9
6
+ metadata.gz: 56b9bb6e1e03cfa079457238d532d9223fdd6adef8438954dfd601afb68b384945b528c2606a3100881f3154e94024ae46f286364c41fce4c23af624a1ebcee3
7
+ data.tar.gz: 3856d03202576bd35465b29e975bb84fe23780695b87442c3ba8b09b7e36deb19cddf6a4ea5299249baf21c9e7b62b61e681221580b6d7a5af6b84858395d8a0
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ = 3.11.0 (2018-08-15)
2
+
3
+ * Disable default compression of sessions over 128 bytes in the sessions plugin (jeremyevans)
4
+
5
+ * Log but otherwise ignore exceptions raised by after processing of error handler response (jeremyevans)
6
+
7
+ * Modify internal before/after processing to avoid plugin load order issues (jeremyevans)
8
+
1
9
  = 3.10.0 (2018-07-18)
2
10
 
3
11
  * Remove flash key from session if new flash is empty when rotating flash (jeremyevans)
@@ -0,0 +1,54 @@
1
+ = Improvements
2
+
3
+ * The order in which internal plugin before and after hooks are run
4
+ when multiple plugins are loaded is now fixed and does not depend
5
+ on the order in which the plugins are loaded. This can prevent
6
+ some issues in cases the plugins were not loaded in the order
7
+ previously recommended in the documentation.
8
+
9
+ Internal plugin before hooks are now run in the following order:
10
+
11
+ * hooks
12
+ * heartbeat
13
+ * static_routing
14
+
15
+ and internal plugin after hooks are now run in the following order:
16
+
17
+ * class_level_routing
18
+ * status_handler
19
+ * head
20
+ * flash
21
+ * session
22
+ * hooks
23
+
24
+ * Default compression of sessions over 128 bytes in length has been
25
+ disabled in the sessions plugin. Compression of sessions must now
26
+ be manually enabled if it is desired by setting :gzip_over to an
27
+ integer.
28
+
29
+ This change is being made to avoid possible compression ratio
30
+ attacks if both sensitive data and user-submitted data are stored in
31
+ the session. Such attacks were mitigated by the sessions plugin's
32
+ default use of padding after compression, and the JSON serialization
33
+ format used, but disabling compression avoids the possibility.
34
+
35
+ This does not affect backwards compatibility, as compressed sessions
36
+ will still be decompressed correctly, unless the size of the session
37
+ cookie when not using compression is over 4096 bytes.
38
+
39
+ = Backwards Compatibility
40
+
41
+ * When using the error_handler plugin, if routing raises an exception that
42
+ is handled by the error handler, but an exception is raised by a plugin
43
+ internal after hook after the error handler has been run, the exception
44
+ will be logged to the rack.errors entry in the environment, but it will
45
+ be otherwise ignored.
46
+
47
+ Exceptions raised inside the error handler will continue to be be raised
48
+ to the application's caller.
49
+
50
+ Additionally, the error_handler plugin no longers call before hooks
51
+ during error handling.
52
+
53
+ * A private Roda#_call method has been added. This could potentially
54
+ cause issues for applications that add their own _call method.
data/lib/roda.rb CHANGED
@@ -239,6 +239,7 @@ class Roda
239
239
  # Build the rack app to use
240
240
  def build_rack_app
241
241
  if block = @route_block
242
+ block = rack_app_route_block(block)
242
243
  app = lambda{|env| new(env).call(&block)}
243
244
  @middleware.reverse_each do |args, bl|
244
245
  mid, *args = args
@@ -248,6 +249,12 @@ class Roda
248
249
  @app = app
249
250
  end
250
251
  end
252
+
253
+ # The route block to use when building the rack app.
254
+ # Can be modified by plugins.
255
+ def rack_app_route_block(block)
256
+ block
257
+ end
251
258
  end
252
259
 
253
260
  # Instance methods for the Roda class.
@@ -277,6 +284,10 @@ class Roda
277
284
  end
278
285
  end
279
286
 
287
+ # Private alias for internal use
288
+ alias _call call
289
+ private :_call
290
+
280
291
  # The environment hash for the current request. Example:
281
292
  #
282
293
  # env['REQUEST_METHOD'] # => 'GET'
@@ -0,0 +1,35 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # Internal after hook module, not for external use.
7
+ # Allows for plugins to configure the order in which
8
+ # after processing is done by using _roda_after_*
9
+ # private instance methods that are called in sorted order.
10
+ module AfterHook # :nodoc:
11
+ module ClassMethods
12
+ # Rebuild the _roda_after method whenever a plugin might
13
+ # have added a _roda_after_* method.
14
+ def include(*)
15
+ res = super
16
+ meths = private_instance_methods.grep(/\A_roda_after_\d\d\z/).sort.map{|s| "#{s}(res)"}.join(';')
17
+ class_eval("def _roda_after(res); #{meths} end", __FILE__, __LINE__)
18
+ private :_roda_after
19
+ res
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ # Run internal after hooks with the response
25
+ def call
26
+ res = super
27
+ ensure
28
+ _roda_after(res)
29
+ end
30
+ end
31
+ end
32
+
33
+ register_plugin(:_after_hook, AfterHook)
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # Internal before hook module, not for external use.
7
+ # Allows for plugins to configure the order in which
8
+ # before processing is done by using _roda_before_*
9
+ # private instance methods that are called in sorted order.
10
+ module BeforeHook # :nodoc:
11
+ # Rebuild the rack app if the rack app already exists,
12
+ # so the before hooks are setup inside the rack app
13
+ # route block.
14
+ def self.configure(app)
15
+ app.instance_exec do
16
+ build_rack_app if @app
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ # Rebuild the _roda_before method whenever a plugin might
22
+ # have added a _roda_before_* method.
23
+ def include(*a)
24
+ res = super
25
+ def_roda_before
26
+ res
27
+ end
28
+
29
+ private
30
+
31
+ # Build a _roda_before method that calls each _roda_before_* method
32
+ # in order.
33
+ def def_roda_before
34
+ meths = private_instance_methods.grep(/\A_roda_before_\d\d\z/).sort.join(';')
35
+ class_eval("def _roda_before; #{meths} end", __FILE__, __LINE__)
36
+ private :_roda_before
37
+ end
38
+
39
+ # Modify rack app route block to use before hook.
40
+ def rack_app_route_block(block)
41
+ lambda do |r|
42
+ _roda_before
43
+ instance_exec(r, &block)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ register_plugin(:_before_hook, BeforeHook)
50
+ end
51
+ end
@@ -52,6 +52,10 @@ class Roda
52
52
  # the normal +route+ class method to define your routing tree. This plugin does make it simpler to
53
53
  # add additional routes after the routing tree has already been defined, though.
54
54
  module ClassLevelRouting
55
+ def self.load_dependencies(app)
56
+ app.plugin :_after_hook
57
+ end
58
+
55
59
  # Initialize the class_routes array when the plugin is loaded. Also, if the application doesn't
56
60
  # currently have a routing block, setup an empty routing block so that things will still work if
57
61
  # a routing block isn't added.
@@ -76,28 +80,29 @@ class Roda
76
80
  end
77
81
 
78
82
  module InstanceMethods
83
+ def initialize(_)
84
+ super
85
+ @_original_remaining_path = @_request.remaining_path
86
+ end
87
+
88
+ private
89
+
79
90
  # If the normal routing tree doesn't handle an action, try each class level route
80
91
  # to see if it matches.
81
- def call
82
- req = @_request
83
- rp = req.remaining_path
84
- result = super
85
-
86
- if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
92
+ def _roda_after_10(result)
93
+ if result && result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
87
94
  # Reset the response so it doesn't inherit the status or any headers from
88
95
  # the original response.
89
96
  @_response.send(:initialize)
90
- super do |r|
97
+ result.replace(_call do |r|
91
98
  opts[:class_level_routes].each do |meth, args, blk|
92
- req.instance_variable_set(:@remaining_path, rp)
99
+ r.instance_variable_set(:@remaining_path, @_original_remaining_path)
93
100
  r.public_send(meth, *args) do |*a|
94
101
  instance_exec(*a, &blk)
95
102
  end
96
103
  end
97
104
  nil
98
- end
99
- else
100
- result
105
+ end)
101
106
  end
102
107
  end
103
108
  end
@@ -28,7 +28,15 @@ class Roda
28
28
  # default status set to 500, before executing the error handler.
29
29
  # The error handler can change the response status if necessary,
30
30
  # as well set headers and/or write to the body, just like a regular
31
- # request.
31
+ # request. After the error handler returns a response, normal after
32
+ # processing of that response occurs, except that an exception during
33
+ # after processing is logged to <tt>env['rack.errors']</tt> but
34
+ # otherwise ignored. This avoids recursive calls into the
35
+ # error_handler. Note that if the error_handler itself raises
36
+ # an exception, the exception will be raised without normal after
37
+ # processing. This can cause some after processing to run twice
38
+ # (once before the error_handler is called and once after) if
39
+ # later after processing raises an exception.
32
40
  #
33
41
  # By default, this plugin handles StandardError and ScriptError.
34
42
  # To override the exception classes it will handle, pass a :classes
@@ -36,6 +44,10 @@ class Roda
36
44
  #
37
45
  # plugin :error_handler, classes: [StandardError, ScriptError, NoMemoryError]
38
46
  module ErrorHandler
47
+ def self.load_dependencies(app, *)
48
+ app.plugin :_after_hook
49
+ end
50
+
39
51
  DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze
40
52
 
41
53
  # If a block is given, automatically call the +error+ method on
@@ -66,7 +78,16 @@ class Roda
66
78
  rescue *opts[:error_handler_classes] => e
67
79
  @_response.send(:initialize)
68
80
  @_response.status = 500
69
- super{handle_error(e)}
81
+ res = _call{handle_error(e)}
82
+ begin
83
+ _roda_after(res)
84
+ rescue => e2
85
+ if errors = env['rack.errors']
86
+ errors.puts "Error in after hook processing of error handler: #{e2.class}: #{e2.message}"
87
+ e2.backtrace.each{|line| errors.puts(line)}
88
+ end
89
+ end
90
+ res
70
91
  end
71
92
 
72
93
  private
@@ -36,6 +36,10 @@ class Roda
36
36
  # flash['a'] # = >'b'
37
37
  # end
38
38
  module Flash
39
+ def self.load_dependencies(app)
40
+ app.plugin :_after_hook
41
+ end
42
+
39
43
  # Simple flash hash, where assiging to the hash updates the flash
40
44
  # used in the following request.
41
45
  class FlashHash < DelegateClass(Hash)
@@ -95,11 +99,11 @@ class Roda
95
99
  @_flash ||= FlashHash.new(session['_flash'] || (session['_flash'] = session.delete(:_flash)))
96
100
  end
97
101
 
102
+ private
103
+
98
104
  # If the routing doesn't raise an error, rotate the flash
99
105
  # hash in the session so the next request has access to it.
100
- def call
101
- res = super
102
-
106
+ def _roda_after_40(_)
103
107
  if f = @_flash
104
108
  f = f.next
105
109
  if f.empty?
@@ -108,8 +112,6 @@ class Roda
108
112
  session['_flash'] = f
109
113
  end
110
114
  end
111
-
112
- res
113
115
  end
114
116
  end
115
117
  end
@@ -37,6 +37,9 @@ class Roda
37
37
  # this plugin those HEAD requests will return a 404 status, which
38
38
  # may prevent search engines from crawling your website.
39
39
  module Head
40
+ def self.load_dependencies(app)
41
+ app.plugin :_after_hook
42
+ end
40
43
 
41
44
  # used to ensure proper resource release on HEAD requests
42
45
  # we do not respond to a to_path method, here.
@@ -56,11 +59,12 @@ class Roda
56
59
  end
57
60
 
58
61
  module InstanceMethods
62
+ private
63
+
59
64
  # Always use an empty response body for head requests, with a
60
65
  # content length of 0.
61
- def call(*)
62
- res = super
63
- if @_request.head?
66
+ def _roda_after_30(res)
67
+ if res && @_request.head?
64
68
  body = res[2]
65
69
  if body.respond_to?(:close)
66
70
  res[2] = CloseLater.new(body)
@@ -68,7 +72,6 @@ class Roda
68
72
  res[2] = EMPTY_ARRAY
69
73
  end
70
74
  end
71
- res
72
75
  end
73
76
  end
74
77
 
@@ -16,20 +16,24 @@ class Roda
16
16
  module Heartbeat
17
17
  HEARTBEAT_RESPONSE = [200, {'Content-Type'=>'text/plain'}.freeze, ['OK'.freeze].freeze].freeze
18
18
 
19
+ def self.load_dependencies(app, opts=OPTS)
20
+ app.plugin :_before_hook
21
+ end
22
+
19
23
  # Set the heartbeat path to the given path.
20
24
  def self.configure(app, opts=OPTS)
21
25
  app.opts[:heartbeat_path] = (opts[:path] || app.opts[:heartbeat_path] || "/heartbeat").dup.freeze
22
26
  end
23
27
 
24
28
  module InstanceMethods
29
+ private
30
+
25
31
  # If the request is for a heartbeat path, return the heartbeat response.
26
- def call
32
+ def _roda_before_20
27
33
  if env['PATH_INFO'] == opts[:heartbeat_path]
28
34
  response = HEARTBEAT_RESPONSE.dup
29
35
  response[1] = Hash[response[1]]
30
- response
31
- else
32
- super
36
+ throw :halt, response
33
37
  end
34
38
  end
35
39
  end
@@ -30,8 +30,14 @@ class Roda
30
30
  # Note that the after hook is called with the rack response array
31
31
  # of status, headers, and body. If it wants to change the response,
32
32
  # it must mutate this argument, calling <tt>response.status=</tt> inside
33
- # an after block will not affect the returned status.
33
+ # an after block will not affect the returned status. Note that after
34
+ # hooks can be called with nil if an exception is raised during routing.
34
35
  module Hooks
36
+ def self.load_dependencies(app)
37
+ app.plugin :_before_hook
38
+ app.plugin :_after_hook
39
+ end
40
+
35
41
  def self.configure(app)
36
42
  app.opts[:before_hook] ||= nil
37
43
  app.opts[:after_hook] ||= nil
@@ -70,21 +76,21 @@ class Roda
70
76
  end
71
77
 
72
78
  module InstanceMethods
73
- # Before routing, execute the before hooks, and
74
- # execute the after hooks before returning.
75
- def call(&block)
76
- res = super do |r|
77
- if b = opts[:before_hook]
78
- instance_exec(&b)
79
- end
79
+ private
80
80
 
81
- instance_exec(r, &block)
82
- end
83
- ensure
81
+ # Run after hooks.
82
+ def _roda_after_90(res)
84
83
  if b = opts[:after_hook]
85
84
  instance_exec(res, &b)
86
85
  end
87
86
  end
87
+
88
+ # Run before hooks.
89
+ def _roda_before_10
90
+ if b = opts[:before_hook]
91
+ instance_exec(&b)
92
+ end
93
+ end
88
94
  end
89
95
  end
90
96
 
@@ -21,8 +21,7 @@ class Roda
21
21
  # way to support sessions in Roda applications.
22
22
  #
23
23
  # The session cookies are encrypted with AES-256-CTR and then signed with HMAC-SHA-256.
24
- # By default, session data over a certain size is compressed to reduced space, and
25
- # is padded to reduce information leaked based on the session size.
24
+ # By default, session data is padded to reduce information leaked based on the session size.
26
25
  #
27
26
  # Sessions are serialized via JSON, so session information should only store data that
28
27
  # allows roundtrips via JSON (String, Integer, Float, Array, Hash, true, false, and nil).
@@ -34,26 +33,20 @@ class Roda
34
33
  # and does not convert all keys to strings.
35
34
  #
36
35
  # All sessions are timestamped and session expiration is enabled by default, with sessions
37
- # being valid for 30 days maximum and 7 days since last use. Session creation time is
36
+ # being valid for 30 days maximum and 7 days since last use by default. Session creation time is
38
37
  # reset whenever the session is empty when serialized and also whenever +clear_session+
39
38
  # is called while processing the request.
40
39
  #
41
- # Session secrets can be rotated, and if so both the cipher and HMAC secrets should be
42
- # rotated at the same time. See options below.
40
+ # Session secrets can be rotated. See options below.
43
41
  #
44
42
  # The sessions plugin can transparently upgrade sessions from Rack::Session::Cookie
45
43
  # if the default Rack::Session::Cookie coder and HMAC are used, see options below.
46
44
  # It is recommended to only enable transparent upgrades for a brief transition period,
47
45
  # and remove support for them once old sessions have converted or timed out.
48
46
  #
49
- # While session data will be compressed by default for sessions over a certain size,
50
- # if the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge
47
+ # If the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge
51
48
  # exception will be raised.
52
49
  #
53
- # If the flash plugin is used, the sessions plugin should be loaded after the flash
54
- # plugin, so that the flash plugin rotates the flash in the session before the sessions
55
- # plugin serializes the session.
56
- #
57
50
  # = Required Options
58
51
  #
59
52
  # The session cookies this plugin uses are both encrypted and signed, so two separate
@@ -70,7 +63,9 @@ class Roda
70
63
  # that. If the +:secure+ option is not present in the hash, then
71
64
  # <tt>secure: true</tt> is also set if the request is made over HTTPS. If this option is
72
65
  # given, it will be merged into the default cookie options.
73
- # :gzip_over :: For session data over this many bytes, compress it with the deflate algorithm (default: 128).
66
+ # :gzip_over :: For session data over this many bytes, compress it with the deflate algorithm (default: nil,
67
+ # so never compress). Note that compression should not be enabled if you are storing data in
68
+ # the session derived from user input and also storing sensitive data in the session.
74
69
  # :key :: The cookie name to use (default: <tt>'roda.session'</tt>)
75
70
  # :max_seconds :: The maximum number of seconds to allow for total session lifetime, starting with when
76
71
  # the session was originally created. Default is <tt>86400*30</tt> (30 days). Can be set to
@@ -140,7 +135,7 @@ class Roda
140
135
  # deflate compression, this contains the deflate compressed data.
141
136
  module Sessions
142
137
  DEFAULT_COOKIE_OPTIONS = {:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.freeze
143
- DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>128, :skip_within=>3600}.freeze
138
+ DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>nil, :skip_within=>3600}.freeze
144
139
  DEFLATE_BIT = 0x1000
145
140
  PADDING_MASK = 0x0fff
146
141
  SESSION_CREATED_AT = 'roda.session.created_at'.freeze
@@ -153,6 +148,10 @@ class Roda
153
148
  class CookieTooLarge < RodaError
154
149
  end
155
150
 
151
+ def self.load_dependencies(app, opts=OPTS)
152
+ app.plugin :_after_hook
153
+ end
154
+
156
155
  # Split given secret into a cipher secret and an hmac secret.
157
156
  def self.split_secret(name, secret)
158
157
  raise RodaError, "sessions plugin :#{name} option must be a String" unless secret.is_a?(String)
@@ -194,19 +193,6 @@ class Roda
194
193
  end
195
194
 
196
195
  module InstanceMethods
197
- # If session information has been set in the request environment,
198
- # update the rack response headers to set the session cookie in
199
- # the response.
200
- def call
201
- res = super
202
-
203
- if session = env['rack.session']
204
- @_request.persist_session(res[1], session)
205
- end
206
-
207
- res
208
- end
209
-
210
196
  # Clear data from the session, and update the request environment
211
197
  # so that the session cookie will use a new creation timestamp
212
198
  # instead of the previous creation timestamp.
@@ -216,6 +202,17 @@ class Roda
216
202
  env.delete(SESSION_UPDATED_AT)
217
203
  nil
218
204
  end
205
+
206
+ private
207
+
208
+ # If session information has been set in the request environment,
209
+ # update the rack response headers to set the session cookie in
210
+ # the response.
211
+ def _roda_after_50(res)
212
+ if res && (session = env['rack.session'])
213
+ @_request.persist_session(res[1], session)
214
+ end
215
+ end
219
216
  end
220
217
 
221
218
  module RequestMethods
@@ -401,8 +398,9 @@ class Roda
401
398
 
402
399
  bitmap = 0
403
400
  json_length = json_data.bytesize
401
+ gzip_over = opts[:gzip_over]
404
402
 
405
- if json_length > opts[:gzip_over]
403
+ if gzip_over && json_length > gzip_over
406
404
  json_data = Zlib.deflate(json_data)
407
405
  json_length = json_data.bytesize
408
406
  bitmap |= DEFLATE_BIT
@@ -48,10 +48,11 @@ class Roda
48
48
  # As shown above, you can use Roda's routing tree methods inside the
49
49
  # static_route block to have shared behavior for different request methods,
50
50
  # while still handling the request methods differently.
51
- #
52
- # Note that if you want to use the static_routing plugin and the hooks
53
- # plugin at the same time, you should load the hooks plugin first.
54
51
  module StaticRouting
52
+ def self.load_dependencies(app)
53
+ app.plugin :_before_hook
54
+ end
55
+
55
56
  def self.configure(app)
56
57
  app.opts[:static_routes] = {}
57
58
  end
@@ -104,15 +105,14 @@ class Roda
104
105
  end
105
106
 
106
107
  module InstanceMethods
108
+ private
109
+
107
110
  # If there is a static routing method for the given path, call it
108
111
  # instead having the routing tree handle the request.
109
- def call(&block)
110
- super do |r|
111
- if route = self.class.static_route_for(r.request_method, r.path_info)
112
- r.static_route(&route)
113
- else
114
- instance_exec(r, &block)
115
- end
112
+ def _roda_before_30
113
+ r = @_request
114
+ if route = self.class.static_route_for(r.request_method, r.path_info)
115
+ r.static_route(&route)
116
116
  end
117
117
  end
118
118
  end
@@ -23,6 +23,10 @@ class Roda
23
23
  # cleared. So if you want to be sure the headers are set even in your block,
24
24
  # you need to reset them in the block.
25
25
  module StatusHandler
26
+ def self.load_dependencies(app)
27
+ app.plugin :_after_hook
28
+ end
29
+
26
30
  def self.configure(app)
27
31
  app.opts[:status_handler] ||= {}
28
32
  end
@@ -41,15 +45,13 @@ class Roda
41
45
  end
42
46
 
43
47
  module InstanceMethods
44
- # If routing returns a response we have a handler for, call that handler.
45
- def call
46
- result = super
48
+ private
47
49
 
48
- if (block = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
50
+ # If routing returns a response we have a handler for, call that handler.
51
+ def _roda_after_20(result)
52
+ if result && (block = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
49
53
  @_response.headers.clear
50
- super(&block)
51
- else
52
- result
54
+ result.replace(_call(&block))
53
55
  end
54
56
  end
55
57
  end
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 10
7
+ RodaMinorVersion = 11
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -144,6 +144,28 @@ describe "error_handler plugin" do
144
144
  proc{req}.must_raise(ArgumentError)
145
145
  end
146
146
 
147
+ it "logs exceptions during after processing of error handler" do
148
+ app(:bare) do
149
+ plugin :error_handler do |e|
150
+ e.message * 2
151
+ end
152
+ plugin :hooks
153
+
154
+ after do
155
+ raise "foo"
156
+ end
157
+
158
+ route do |r|
159
+ ''
160
+ end
161
+ end
162
+
163
+ errors = StringIO.new
164
+ body('rack.errors'=>errors).must_equal 'foofoo'
165
+ errors.rewind
166
+ errors.read.split("\n").first.must_equal "Error in after hook processing of error handler: RuntimeError: foo"
167
+ end
168
+
147
169
  it "has access to current remaining_path" do
148
170
  app(:bare) do
149
171
  plugin :error_handler do |e|
@@ -3,47 +3,49 @@ require_relative "../spec_helper"
3
3
  describe "flash plugin" do
4
4
  include CookieJar
5
5
 
6
- it "flash.now[] sets flash for current page" do
7
- app(:bare) do
8
- send(*DEFAULT_SESSION_ARGS)
9
- plugin :flash
10
-
11
- route do |r|
12
- r.on do
13
- flash.now['a'] = 'b'
14
- flash['a']
6
+ [lambda{send(*DEFAULT_SESSION_ARGS); plugin :flash},
7
+ lambda{plugin :flash; send(*DEFAULT_SESSION_ARGS)}].each do |config|
8
+
9
+ it "flash.now[] sets flash for current page" do
10
+ app(:bare) do
11
+ instance_exec(&config)
12
+
13
+ route do |r|
14
+ r.on do
15
+ flash.now['a'] = 'b'
16
+ flash['a']
17
+ end
15
18
  end
16
19
  end
17
- end
18
20
 
19
- body.must_equal 'b'
20
- end
21
+ body.must_equal 'b'
22
+ end
21
23
 
22
- it "flash[] sets flash for next page" do
23
- app(:bare) do
24
- plugin :flash
25
- send(*DEFAULT_SESSION_ARGS)
24
+ it "flash[] sets flash for next page" do
25
+ app(:bare) do
26
+ instance_exec(&config)
26
27
 
27
- route do |r|
28
- r.get('a'){"c#{flash['a']}"}
29
- r.get('f'){flash; session['_flash'].inspect}
28
+ route do |r|
29
+ r.get('a'){"c#{flash['a']}"}
30
+ r.get('f'){flash; session['_flash'].inspect}
30
31
 
31
- flash['a'] = "b#{flash['a']}"
32
- flash['a'] || ''
32
+ flash['a'] = "b#{flash['a']}"
33
+ flash['a'] || ''
34
+ end
33
35
  end
34
- end
35
36
 
36
- body.must_equal ''
37
- body.must_equal 'b'
38
- body.must_equal 'bb'
37
+ body.must_equal ''
38
+ body.must_equal 'b'
39
+ body.must_equal 'bb'
39
40
 
40
- body('/a').must_equal 'cbbb'
41
- body.must_equal ''
42
- body.must_equal 'b'
43
- body.must_equal 'bb'
41
+ body('/a').must_equal 'cbbb'
42
+ body.must_equal ''
43
+ body.must_equal 'b'
44
+ body.must_equal 'bb'
44
45
 
45
- body('/f').must_equal '{"a"=>"bbb"}'
46
- body('/f').must_equal 'nil'
46
+ body('/f').must_equal '{"a"=>"bbb"}'
47
+ body('/f').must_equal 'nil'
48
+ end
47
49
  end
48
50
  end
49
51
 
@@ -100,4 +100,32 @@ describe "hooks plugin" do
100
100
  end
101
101
  status.must_equal 201
102
102
  end
103
+
104
+ it "works with error plugin when loaded first" do
105
+ app.plugin(:error_handler){|e| "error"}
106
+ app.before do
107
+ raise "before" if @_request.path == '/b'
108
+ end
109
+ app.after do
110
+ raise "after" if @_request.path == '/a'
111
+ end
112
+ body('/a').must_equal "error"
113
+ body('/b').must_equal "error"
114
+ end
115
+
116
+ it "works with error plugin when loaded after" do
117
+ app(:bare) do
118
+ plugin(:error_handler){|e| "error"}
119
+ plugin :hooks
120
+ before do
121
+ raise "before" if @_request.path == '/b'
122
+ end
123
+ after do
124
+ raise "after" if @_request.path == '/a'
125
+ end
126
+ route{}
127
+ end
128
+ body('/a').must_equal "error"
129
+ body('/b').must_equal "error"
130
+ end
103
131
  end
@@ -205,15 +205,15 @@ describe "sessions plugin" do
205
205
 
206
206
  it "compresses data over a certain size by default" do
207
207
  long = 'b'*8192
208
+ proc{body("/s/foo/#{long}")}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
209
+
210
+ @app.plugin(:sessions, :gzip_over=>8000)
208
211
  body("/s/foo/#{long}").must_equal long
209
- body("/g/foo").must_equal long
212
+ body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long
210
213
 
211
214
  @app.plugin(:sessions, :gzip_over=>15000)
212
215
  proc{body("/g/foo", 'QUERY_STRING'=>'sut=3700')}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
213
216
 
214
- @app.plugin(:sessions, :gzip_over=>8000)
215
- body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long
216
-
217
217
  errors.must_equal []
218
218
  end
219
219
 
@@ -66,6 +66,26 @@ describe "static_routing plugin" do
66
66
  a.must_equal [1,3,2]
67
67
  end
68
68
 
69
+ it "works with hooks plugin if loaded before" do
70
+ a = []
71
+ app(:bare) do
72
+ plugin :static_routing
73
+ plugin :hooks
74
+
75
+ before{a << 1}
76
+ after{a << 2}
77
+
78
+ static_route "/foo" do |r|
79
+ a << 3
80
+ "bar"
81
+ end
82
+
83
+ route{}
84
+ end
85
+ body('/foo').must_equal 'bar'
86
+ a.must_equal [1,3,2]
87
+ end
88
+
69
89
  it "does not allow placeholders in static routes" do
70
90
  app(:bare) do
71
91
  plugin :static_routing
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.10.0
4
+ version: 3.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-18 00:00:00.000000000 Z
11
+ date: 2018-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -206,6 +206,7 @@ extra_rdoc_files:
206
206
  - doc/release_notes/3.8.0.txt
207
207
  - doc/release_notes/3.9.0.txt
208
208
  - doc/release_notes/3.10.0.txt
209
+ - doc/release_notes/3.11.0.txt
209
210
  files:
210
211
  - CHANGELOG
211
212
  - MIT-LICENSE
@@ -250,6 +251,7 @@ files:
250
251
  - doc/release_notes/3.0.0.txt
251
252
  - doc/release_notes/3.1.0.txt
252
253
  - doc/release_notes/3.10.0.txt
254
+ - doc/release_notes/3.11.0.txt
253
255
  - doc/release_notes/3.2.0.txt
254
256
  - doc/release_notes/3.3.0.txt
255
257
  - doc/release_notes/3.4.0.txt
@@ -259,6 +261,8 @@ files:
259
261
  - doc/release_notes/3.8.0.txt
260
262
  - doc/release_notes/3.9.0.txt
261
263
  - lib/roda.rb
264
+ - lib/roda/plugins/_after_hook.rb
265
+ - lib/roda/plugins/_before_hook.rb
262
266
  - lib/roda/plugins/_symbol_regexp_matchers.rb
263
267
  - lib/roda/plugins/all_verbs.rb
264
268
  - lib/roda/plugins/assets.rb