roda 3.10.0 → 3.11.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.
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