@@ -198,6 +198,8 @@ def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]):
198198 return f"{ self ._name } :{ self .id } :{ path } "
199199
200200 def _reset_app (self ):
201+ self ._get_id_by_root_path_map .clear_cache (self )
202+ self ._get_id_for_path .clear_cache (self )
201203 self ._reset_app_cache_marker .clear_cache (self )
202204
203205 @tools .ormcache ()
@@ -211,24 +213,71 @@ def _reset_app_cache_marker(self):
211213 """
212214
213215 @api .model
214- @tools .ormcache ("path" )
215- def get_endpoint (self , path ):
216- # try to match the request url with the most similar endpoint
217- endpoints_by_length = self .search ([]).sorted (
218- lambda fe : len (fe .root_path ), reverse = True
216+ def _normalize_url_path (self , path ) -> str :
217+ """
218+ Normalize a URL path:
219+ * Remove redundant slashes,
220+ * Remove trailing slash (unless it's the root),
221+ * Lowercase for case-insensitive matching
222+ """
223+ parts = [part .lower () for part in path .strip ().split ("/" ) if part ]
224+ return "/" + "/" .join (parts )
225+
226+ @api .model
227+ def _is_suburl (self , path , prefix ) -> bool :
228+ """
229+ Check if 'path' is a subpath of 'prefix' in URL logic:
230+ * Must start with the prefix followed by a slash
231+ This will ensure that the matching is done one the path
232+ parts and ensures that e.g. /a/b is not prefix of /a/bc.
233+ """
234+ path = self ._normalize_url_path (path )
235+ prefix = self ._normalize_url_path (prefix )
236+
237+ if path == prefix :
238+ return True
239+ if path .startswith (prefix + "/" ):
240+ return True
241+ return False
242+
243+ @api .model
244+ def _find_first_matching_url_path (self , paths , prefix ) -> str | None :
245+ """
246+ Return the first path that is a subpath of 'prefix',
247+ ordered by longest URL path first (most number of segments).
248+ """
249+ # Sort by number of segments (shallowest first)
250+ sorted_paths = sorted (
251+ paths ,
252+ key = lambda p : len (self ._normalize_url_path (p ).split ("/" )),
253+ reverse = True ,
219254 )
220- endpoint = False
221- while endpoints_by_length :
222- candidate_endpoint = endpoints_by_length [0 ]
223- if path .startswith (candidate_endpoint .root_path ):
224- endpoint = candidate_endpoint
225- break
226- endpoints_by_length -= candidate_endpoint
227- return endpoint
255+
256+ for path in sorted_paths :
257+ if self ._is_suburl (prefix , path ):
258+ return path
259+ return None
260+
261+ @api .model
262+ @tools .ormcache ()
263+ def _get_id_by_root_path_map (self ):
264+ return {r .root_path : r .id for r in self .search ([])}
265+
266+ @api .model
267+ @tools .ormcache ("path" )
268+ def _get_id_for_path (self , path ):
269+ id_by_path = self ._get_id_by_root_path_map ()
270+ root_path = self ._find_first_matching_url_path (id_by_path .keys (), path )
271+ return id_by_path .get (root_path )
272+
273+ @api .model
274+ def _get_endpoint (self , path ):
275+ id_ = self ._get_id_for_path (path )
276+ return self .browse (id_ ) if id_ else None
228277
229278 @api .model
230279 def get_app (self , path ):
231- record = self .get_endpoint (path )
280+ record = self ._get_endpoint (path )
232281 if not record :
233282 return None
234283 app = FastAPI ()
@@ -254,7 +303,7 @@ def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None:
254303 @api .model
255304 @tools .ormcache ("path" )
256305 def get_uid (self , path ):
257- record = self .get_endpoint (path )
306+ record = self ._get_endpoint (path )
258307 if not record :
259308 return None
260309 return record .user_id .id
0 commit comments