@@ -3,11 +3,29 @@ module Kemal
33 class FilterHandler
44 include HTTP ::Handler
55 INSTANCE = new
6- property tree
6+
7+ # Path used to represent wildcard filters that apply to all routes
8+ private WILDCARD_PATH = " *"
9+
10+ @tree : Radix ::Tree (Array (FilterBlock ))
11+
12+ # Hash cache for exact path filters to avoid repeated tree lookups
13+ # Key format: "/#{type}/#{verb}/#{path}" (e.g., "/before/ALL/*")
14+ @exact_filters : Hash (String , Array (FilterBlock ))
15+
16+ def tree
17+ @tree
18+ end
19+
20+ def tree = (tree : Radix ::Tree (Array (FilterBlock )))
21+ @tree = tree
22+ @exact_filters = Hash (String , Array (FilterBlock )).new
23+ end
724
825 # This middleware is lazily instantiated and added to the handlers as soon as a call to `after_X` or `before_X` is made.
926 def initialize
1027 @tree = Radix ::Tree (Array (FilterBlock )).new
28+ @exact_filters = Hash (String , Array (FilterBlock )).new
1129 Kemal .config.add_filter_handler(self )
1230 end
1331
@@ -31,15 +49,21 @@ module Kemal
3149 context
3250 end
3351
34- # :nodoc: This shouldn't be called directly, it's not private because
35- # I need to call it for testing purpose since I can't call the macros in the spec.
36- # It adds the block for the corresponding verb/path/type combination to the tree.
52+ # :nodoc:
53+ # This shouldn't be called directly, it's not private because I need to call it for testing purpose since I can't call the macros in the spec.
54+ #
55+ # Registers a filter block for the given verb/path/type combination.
56+ # Uses @exact_filters hash for O(1) lookup when adding multiple filters to the same path.
3757 def _add_route_filter (verb : String , path, type , & block : HTTP ::Server ::Context - > _)
38- lookup = lookup_filters_for_path_type(verb, path, type )
39- if lookup.found? && lookup.payload.is_a?(Array (FilterBlock ))
40- lookup.payload << FilterBlock .new(& block)
58+ key = radix_path(verb, path, type )
59+
60+ if filters = @exact_filters [key]?
61+ filters << FilterBlock .new(& block)
4162 else
42- @tree .add radix_path(verb, path, type ), [FilterBlock .new(& block)]
63+ filters = [FilterBlock .new(& block)]
64+ @exact_filters [key] = filters
65+
66+ @tree .add key, filters
4367 end
4468 end
4569
@@ -57,8 +81,24 @@ module Kemal
5781 _add_route_filter verb, path, :after , & block
5882 end
5983
60- # This will fetch the block for the verb/path/type from the tree and call it.
84+ # Executes filters for a given path, ensuring global wildcard filters run first.
85+ #
86+ # Execution order:
87+ # 1. Global wildcard filters ("*") - if path is not already a wildcard
88+ # 2. Exact path filters - filters registered for the specific path
89+ #
90+ # This ensures that global filters (like `before_all`) always execute,
91+ # while namespace-specific filters only apply to their registered paths.
6192 private def call_block_for_path_type (verb : String ?, path : String , type , context : HTTP ::Server ::Context )
93+ if path != WILDCARD_PATH
94+ call_block_for_exact_path_type(verb, " *" , type , context)
95+ end
96+
97+ # Executes all filter blocks registered for a specific verb/path/type combination
98+ call_block_for_exact_path_type(verb, path, type , context)
99+ end
100+
101+ private def call_block_for_exact_path_type (verb : String ?, path : String , type , context : HTTP ::Server ::Context )
62102 lookup = lookup_filters_for_path_type(verb, path, type )
63103 if lookup.found? && lookup.payload.is_a? Array (FilterBlock )
64104 blocks = lookup.payload
0 commit comments