roda 3.17.0 → 3.18.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 +4 -4
- data/CHANGELOG +48 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +22 -4
- data/doc/release_notes/3.18.0.txt +170 -0
- data/lib/roda.rb +249 -26
- data/lib/roda/plugins/_after_hook.rb +4 -26
- data/lib/roda/plugins/_before_hook.rb +30 -2
- data/lib/roda/plugins/branch_locals.rb +2 -2
- data/lib/roda/plugins/class_level_routing.rb +9 -7
- data/lib/roda/plugins/default_headers.rb +15 -1
- data/lib/roda/plugins/default_status.rb +9 -10
- data/lib/roda/plugins/direct_call.rb +38 -0
- data/lib/roda/plugins/error_email.rb +1 -1
- data/lib/roda/plugins/error_handler.rb +37 -11
- data/lib/roda/plugins/hooks.rb +28 -30
- data/lib/roda/plugins/mail_processor.rb +16 -11
- data/lib/roda/plugins/mailer.rb +1 -1
- data/lib/roda/plugins/middleware.rb +13 -3
- data/lib/roda/plugins/multi_route.rb +3 -3
- data/lib/roda/plugins/named_templates.rb +4 -4
- data/lib/roda/plugins/path.rb +13 -8
- data/lib/roda/plugins/render.rb +2 -2
- data/lib/roda/plugins/route_block_args.rb +4 -3
- data/lib/roda/plugins/route_csrf.rb +9 -4
- data/lib/roda/plugins/sessions.rb +2 -1
- data/lib/roda/plugins/shared_vars.rb +1 -1
- data/lib/roda/plugins/static_routing.rb +7 -17
- data/lib/roda/plugins/status_handler.rb +5 -3
- data/lib/roda/plugins/view_options.rb +2 -2
- data/lib/roda/version.rb +1 -1
- data/spec/define_roda_method_spec.rb +257 -0
- data/spec/plugin/class_level_routing_spec.rb +0 -27
- data/spec/plugin/default_headers_spec.rb +7 -0
- data/spec/plugin/default_status_spec.rb +31 -1
- data/spec/plugin/direct_call_spec.rb +28 -0
- data/spec/plugin/error_handler_spec.rb +27 -0
- data/spec/plugin/hooks_spec.rb +21 -0
- data/spec/plugin/middleware_spec.rb +108 -36
- data/spec/plugin/multi_route_spec.rb +12 -0
- data/spec/plugin/route_csrf_spec.rb +27 -0
- data/spec/plugin/sessions_spec.rb +26 -1
- data/spec/plugin/static_routing_spec.rb +25 -3
- data/spec/plugin/status_handler_spec.rb +17 -0
- data/spec/route_spec.rb +39 -0
- data/spec/spec_helper.rb +2 -2
- metadata +9 -3
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'error_handler'
|
|
4
|
+
|
|
3
5
|
#
|
|
4
6
|
class Roda
|
|
5
7
|
module RodaPlugins
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
# after processing is done by using _roda_after_*
|
|
9
|
-
# private instance methods that are called in sorted order.
|
|
10
|
-
# Loaded automatically by the base library if any _roda_after_*
|
|
11
|
-
# methods are defined.
|
|
12
|
-
module AfterHook # :nodoc:
|
|
13
|
-
# Module for internal after hook support.
|
|
14
|
-
module InstanceMethods
|
|
15
|
-
# Run internal after hooks with the response
|
|
16
|
-
def call
|
|
17
|
-
res = super
|
|
18
|
-
ensure
|
|
19
|
-
_roda_after(res)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
# Empty roda_after method, so nothing breaks if the module is included.
|
|
25
|
-
# This method will be overridden in most classes using this module.
|
|
26
|
-
def _roda_after(res)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
register_plugin(:_after_hook, AfterHook)
|
|
8
|
+
# RODA4: Remove
|
|
9
|
+
register_plugin(:_after_hook, ErrorHandler)
|
|
32
10
|
end
|
|
33
11
|
end
|
|
@@ -3,9 +3,37 @@
|
|
|
3
3
|
#
|
|
4
4
|
class Roda
|
|
5
5
|
module RodaPlugins
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
+
# Loaded automatically by the base library if any _roda_before_*
|
|
11
|
+
# methods are defined.
|
|
8
12
|
module BeforeHook # :nodoc:
|
|
13
|
+
module InstanceMethods
|
|
14
|
+
# Run internal before hooks - Old Dispatch API.
|
|
15
|
+
def call(&block)
|
|
16
|
+
# RODA4: Remove
|
|
17
|
+
super do
|
|
18
|
+
_roda_before
|
|
19
|
+
instance_exec(@_request, &block) # call Fallback
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Run internal before hooks before running the main
|
|
24
|
+
# roda route.
|
|
25
|
+
def _roda_run_main_route(r)
|
|
26
|
+
_roda_before
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Default empty implementation of _roda_before, usually
|
|
33
|
+
# overridden by Roda.def_roda_before.
|
|
34
|
+
def _roda_before
|
|
35
|
+
end
|
|
36
|
+
end
|
|
9
37
|
end
|
|
10
38
|
|
|
11
39
|
register_plugin(:_before_hook, BeforeHook)
|
|
@@ -31,7 +31,7 @@ class Roda
|
|
|
31
31
|
# Update the default layout locals to use in this branch.
|
|
32
32
|
def set_layout_locals(opts)
|
|
33
33
|
if locals = @_layout_locals
|
|
34
|
-
@_layout_locals =
|
|
34
|
+
@_layout_locals = locals.merge(opts)
|
|
35
35
|
else
|
|
36
36
|
@_layout_locals = opts
|
|
37
37
|
end
|
|
@@ -40,7 +40,7 @@ class Roda
|
|
|
40
40
|
# Update the default view locals to use in this branch.
|
|
41
41
|
def set_view_locals(opts)
|
|
42
42
|
if locals = @_view_locals
|
|
43
|
-
@_view_locals =
|
|
43
|
+
@_view_locals = locals.merge(opts)
|
|
44
44
|
else
|
|
45
45
|
@_view_locals = opts
|
|
46
46
|
end
|
|
@@ -62,9 +62,10 @@ class Roda
|
|
|
62
62
|
|
|
63
63
|
module ClassMethods
|
|
64
64
|
# Define routing methods that will store class level routes.
|
|
65
|
-
[:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |
|
|
66
|
-
define_method(
|
|
67
|
-
|
|
65
|
+
[:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |request_meth|
|
|
66
|
+
define_method(request_meth) do |*args, &block|
|
|
67
|
+
meth = define_roda_method("class_level_routing_#{request_meth}", :any, &block)
|
|
68
|
+
opts[:class_level_routes] << [request_meth, args, meth].freeze
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -91,11 +92,12 @@ class Roda
|
|
|
91
92
|
# the original response.
|
|
92
93
|
@_response.send(:initialize)
|
|
93
94
|
@_response.status = nil
|
|
94
|
-
result.replace(
|
|
95
|
-
|
|
95
|
+
result.replace(_roda_handle_route do
|
|
96
|
+
r = @_request
|
|
97
|
+
opts[:class_level_routes].each do |request_meth, args, meth|
|
|
96
98
|
r.instance_variable_set(:@remaining_path, @_original_remaining_path)
|
|
97
|
-
r.public_send(
|
|
98
|
-
|
|
99
|
+
r.public_send(request_meth, *args) do |*a|
|
|
100
|
+
send(meth, *a)
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
nil
|
|
@@ -23,7 +23,21 @@ class Roda
|
|
|
23
23
|
module DefaultHeaders
|
|
24
24
|
# Merge the given headers into the existing default headers, if any.
|
|
25
25
|
def self.configure(app, headers={})
|
|
26
|
-
app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
|
|
26
|
+
headers = app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
|
|
27
|
+
|
|
28
|
+
if headers.all?{|k, v| k.is_a?(String) && v.is_a?(String)}
|
|
29
|
+
response_class = app::RodaResponse
|
|
30
|
+
owner = response_class.instance_method(:set_default_headers).owner
|
|
31
|
+
if owner == Base::ResponseMethods || (owner == response_class && app.opts[:set_default_headers_overridder] == response_class)
|
|
32
|
+
app.opts[:set_default_headers_overridder] = response_class
|
|
33
|
+
response_class.class_eval(<<-END, __FILE__, __LINE__+1)
|
|
34
|
+
def set_default_headers
|
|
35
|
+
h = @headers
|
|
36
|
+
#{headers.map{|k,v| "h[#{k.inspect}] ||= #{v.inspect}"}.join('; ')}
|
|
37
|
+
end
|
|
38
|
+
END
|
|
39
|
+
end
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
|
|
29
43
|
module ClassMethods
|
|
@@ -7,8 +7,6 @@ class Roda
|
|
|
7
7
|
# return a response status integer. This integer will be used as
|
|
8
8
|
# the default response status (usually 200) if the body has been
|
|
9
9
|
# written to, and you have not explicitly set a response status.
|
|
10
|
-
# The block given to the block is instance_execed in the context
|
|
11
|
-
# of the response.
|
|
12
10
|
#
|
|
13
11
|
# Example:
|
|
14
12
|
#
|
|
@@ -19,15 +17,16 @@ class Roda
|
|
|
19
17
|
module DefaultStatus
|
|
20
18
|
def self.configure(app, &block)
|
|
21
19
|
raise RodaError, "default_status plugin requires a block" unless block
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
if check_arity = app.opts.fetch(:check_arity, true)
|
|
21
|
+
unless block.arity == 0
|
|
22
|
+
if check_arity == :warn
|
|
23
|
+
RodaPlugins.warn "Arity mismatch in block passed to plugin :default_status. Expected Arity 0, but arguments required for #{block.inspect}"
|
|
24
|
+
end
|
|
25
|
+
b = block
|
|
26
|
+
block = lambda{instance_exec(&b)} # Fallback
|
|
27
|
+
end
|
|
30
28
|
end
|
|
29
|
+
app::RodaResponse.send(:define_method, :default_status, &block)
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
class Roda
|
|
5
|
+
module RodaPlugins
|
|
6
|
+
# The direct_call plugin makes the call class method skip the middleware stack
|
|
7
|
+
# (app.call will still call the middleware).
|
|
8
|
+
# This can be used as an optimization, as the Roda class itself can be used
|
|
9
|
+
# as the callable, which is faster than using a lambda.
|
|
10
|
+
module DirectCall
|
|
11
|
+
def self.configure(app)
|
|
12
|
+
app.send(:build_rack_app)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
# Call the application without middlware.
|
|
17
|
+
def call(env)
|
|
18
|
+
new(env)._roda_handle_main_route
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# If new_api is true, use the receiver as the base rack app for better
|
|
24
|
+
# performance.
|
|
25
|
+
def base_rack_app_callable(new_api=true)
|
|
26
|
+
if new_api
|
|
27
|
+
self
|
|
28
|
+
else
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
register_plugin(:direct_call, DirectCall)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
@@ -123,7 +123,7 @@ END
|
|
|
123
123
|
def error_email_content(exception)
|
|
124
124
|
email_opts = self.class.opts[:error_email]
|
|
125
125
|
headers = email_opts[:default_headers].call(email_opts, exception)
|
|
126
|
-
headers =
|
|
126
|
+
headers = headers.merge(email_opts[:headers])
|
|
127
127
|
headers = headers.map{|k,v| "#{k}: #{v.gsub(/\r?\n/m, "\r\n ")}"}.sort.join("\r\n")
|
|
128
128
|
body = email_opts[:body].call(self, exception)
|
|
129
129
|
"#{headers}\r\n\r\n#{body}"
|
|
@@ -44,10 +44,6 @@ class Roda
|
|
|
44
44
|
#
|
|
45
45
|
# plugin :error_handler, classes: [StandardError, ScriptError, NoMemoryError]
|
|
46
46
|
module ErrorHandler
|
|
47
|
-
def self.load_dependencies(app, *)
|
|
48
|
-
app.plugin :_after_hook
|
|
49
|
-
end
|
|
50
|
-
|
|
51
47
|
DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze
|
|
52
48
|
|
|
53
49
|
# If a block is given, automatically call the +error+ method on
|
|
@@ -72,13 +68,45 @@ class Roda
|
|
|
72
68
|
|
|
73
69
|
module InstanceMethods
|
|
74
70
|
# If an error occurs, set the response status to 500 and call
|
|
75
|
-
# the error handler.
|
|
71
|
+
# the error handler. Old Dispatch API.
|
|
76
72
|
def call
|
|
77
|
-
|
|
73
|
+
# RODA4: Remove
|
|
74
|
+
begin
|
|
75
|
+
res = super
|
|
76
|
+
ensure
|
|
77
|
+
_roda_after(res)
|
|
78
|
+
end
|
|
79
|
+
rescue *opts[:error_handler_classes] => e
|
|
80
|
+
_handle_error(e)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# If an error occurs, set the response status to 500 and call
|
|
84
|
+
# the error handler.
|
|
85
|
+
def _roda_handle_main_route
|
|
86
|
+
begin
|
|
87
|
+
res = super
|
|
88
|
+
ensure
|
|
89
|
+
_roda_after(res)
|
|
90
|
+
end
|
|
78
91
|
rescue *opts[:error_handler_classes] => e
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
_handle_error(e)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Default empty implementation of _roda_after, usually
|
|
98
|
+
# overridden by Roda.def_roda_before.
|
|
99
|
+
def _roda_after(res)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Handle the given exception using handle_error, using a default status
|
|
103
|
+
# of 500. Run after hooks on the rack response, but if any error occurs
|
|
104
|
+
# when doing so, log the error using rack.errors and return the response.
|
|
105
|
+
def _handle_error(e)
|
|
106
|
+
res = @_response
|
|
107
|
+
res.send(:initialize)
|
|
108
|
+
res.status = 500
|
|
109
|
+
res = _roda_handle_route{handle_error(e)}
|
|
82
110
|
begin
|
|
83
111
|
_roda_after(res)
|
|
84
112
|
rescue => e2
|
|
@@ -90,8 +118,6 @@ class Roda
|
|
|
90
118
|
res
|
|
91
119
|
end
|
|
92
120
|
|
|
93
|
-
private
|
|
94
|
-
|
|
95
121
|
# By default, have the error handler reraise the error, so using
|
|
96
122
|
# the plugin without installing an error handler doesn't change
|
|
97
123
|
# behavior.
|
data/lib/roda/plugins/hooks.rb
CHANGED
|
@@ -34,57 +34,55 @@ class Roda
|
|
|
34
34
|
# hooks can be called with nil if an exception is raised during routing.
|
|
35
35
|
module Hooks
|
|
36
36
|
def self.configure(app)
|
|
37
|
-
app.opts[:
|
|
38
|
-
app.opts[:
|
|
37
|
+
app.opts[:after_hooks] ||= []
|
|
38
|
+
app.opts[:before_hooks] ||= []
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
module ClassMethods
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
# Freeze the array of hook methods when freezing the app.
|
|
43
|
+
def freeze
|
|
44
|
+
opts[:after_hooks].freeze
|
|
45
|
+
opts[:before_hooks].freeze
|
|
46
|
+
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Add an after hook.
|
|
46
51
|
def after(&block)
|
|
47
|
-
opts[:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
instance_exec(res, &block)
|
|
51
|
-
end
|
|
52
|
+
opts[:after_hooks] << define_roda_method("after_hook", 1, &block)
|
|
53
|
+
if opts[:after_hooks].length == 1
|
|
54
|
+
class_eval("alias _roda_after_80__hooks #{opts[:after_hooks].first}", __FILE__, __LINE__)
|
|
52
55
|
else
|
|
53
|
-
|
|
56
|
+
class_eval("def _roda_after_80__hooks(res) #{opts[:after_hooks].map{|m| "#{m}(res)"}.join(';')} end", __FILE__, __LINE__)
|
|
54
57
|
end
|
|
58
|
+
private :_roda_after_80__hooks
|
|
59
|
+
def_roda_after
|
|
60
|
+
nil
|
|
55
61
|
end
|
|
56
62
|
|
|
57
|
-
# Add a before hook.
|
|
58
|
-
# use a proc that instance_execs the give before proc and
|
|
59
|
-
# then instance_execs the existing before proc, so that the given
|
|
60
|
-
# before proc always executes before the previous one.
|
|
63
|
+
# Add a before hook.
|
|
61
64
|
def before(&block)
|
|
62
|
-
opts[:before_hook
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
instance_exec(&b)
|
|
66
|
-
end
|
|
65
|
+
opts[:before_hooks].unshift(define_roda_method("before_hook", 0, &block))
|
|
66
|
+
if opts[:before_hooks].length == 1
|
|
67
|
+
class_eval("alias _roda_before_10__hooks #{opts[:before_hooks].first}", __FILE__, __LINE__)
|
|
67
68
|
else
|
|
68
|
-
|
|
69
|
+
class_eval("def _roda_before_10__hooks; #{opts[:before_hooks].join(';')} end", __FILE__, __LINE__)
|
|
69
70
|
end
|
|
71
|
+
private :_roda_before_10__hooks
|
|
72
|
+
def_roda_before
|
|
73
|
+
nil
|
|
70
74
|
end
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
module InstanceMethods
|
|
74
78
|
private
|
|
75
79
|
|
|
76
|
-
#
|
|
80
|
+
# Default method if no after hooks are defined.
|
|
77
81
|
def _roda_after_80__hooks(res)
|
|
78
|
-
if b = opts[:after_hook]
|
|
79
|
-
instance_exec(res, &b)
|
|
80
|
-
end
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
#
|
|
84
|
+
# Default method if no before hooks are defined.
|
|
84
85
|
def _roda_before_10__hooks
|
|
85
|
-
if b = opts[:before_hook]
|
|
86
|
-
instance_exec(&b)
|
|
87
|
-
end
|
|
88
86
|
end
|
|
89
87
|
end
|
|
90
88
|
end
|
|
@@ -301,7 +301,7 @@ class Roda
|
|
|
301
301
|
|
|
302
302
|
begin
|
|
303
303
|
begin
|
|
304
|
-
scope.process_mail
|
|
304
|
+
scope.process_mail
|
|
305
305
|
rescue UnhandledMail
|
|
306
306
|
scope.unhandled_mail_hook
|
|
307
307
|
else
|
|
@@ -329,16 +329,23 @@ class Roda
|
|
|
329
329
|
def rcpt(*addresses, &block)
|
|
330
330
|
opts[:mail_processor_string_routes] ||= {}
|
|
331
331
|
opts[:mail_processor_regexp_routes] ||= {}
|
|
332
|
+
string_meth = nil
|
|
333
|
+
regexp_meth = nil
|
|
332
334
|
addresses.each do |address|
|
|
333
335
|
key = case address
|
|
334
336
|
when String
|
|
335
|
-
|
|
337
|
+
unless string_meth
|
|
338
|
+
string_meth = define_roda_method("mail_processor_string_route_#{address}", 1, &convert_route_block(block))
|
|
339
|
+
end
|
|
340
|
+
opts[:mail_processor_string_routes][address] = string_meth
|
|
336
341
|
when Regexp
|
|
337
|
-
|
|
342
|
+
unless regexp_meth
|
|
343
|
+
regexp_meth = define_roda_method("mail_processor_regexp_route_#{address}", :any, &convert_route_block(block))
|
|
344
|
+
end
|
|
345
|
+
opts[:mail_processor_regexp_routes][address] = regexp_meth
|
|
338
346
|
else
|
|
339
347
|
raise RodaError, "invalid address format passed to rcpt, should be Array or String"
|
|
340
348
|
end
|
|
341
|
-
opts[key][address] = block
|
|
342
349
|
end
|
|
343
350
|
nil
|
|
344
351
|
end
|
|
@@ -379,25 +386,23 @@ class Roda
|
|
|
379
386
|
addresses = mail_recipients
|
|
380
387
|
|
|
381
388
|
addresses.each do |address|
|
|
382
|
-
if
|
|
383
|
-
|
|
389
|
+
if meth = string_routes[address.to_s.downcase]
|
|
390
|
+
_roda_handle_route{send(meth, @_request)}
|
|
384
391
|
return
|
|
385
392
|
end
|
|
386
393
|
end
|
|
387
394
|
|
|
388
|
-
opts[:mail_processor_regexp_routes].each do |regexp,
|
|
395
|
+
opts[:mail_processor_regexp_routes].each do |regexp, meth|
|
|
389
396
|
addresses.each do |address|
|
|
390
397
|
if md = regexp.match(address)
|
|
391
|
-
|
|
392
|
-
instance_exec(r, *md.captures, &blk)
|
|
393
|
-
end
|
|
398
|
+
_roda_handle_route{send(meth, @_request, *md.captures)}
|
|
394
399
|
return
|
|
395
400
|
end
|
|
396
401
|
end
|
|
397
402
|
end
|
|
398
403
|
end
|
|
399
404
|
|
|
400
|
-
|
|
405
|
+
_roda_handle_main_route
|
|
401
406
|
|
|
402
407
|
nil
|
|
403
408
|
end
|