fastapi-guard 7.2.1__tar.gz → 7.2.2__tar.gz

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.
@@ -2,6 +2,7 @@ version_selector: true
2
2
  title_switch: true
3
3
  versions_file: docs/versions/versions.json
4
4
  versions:
5
+ - 7.2.2
5
6
  - 7.2.1
6
7
  - 7.2.0
7
8
  - 7.1.1
@@ -47,4 +48,4 @@ versions:
47
48
  - 0.3.2
48
49
  - latest
49
50
  aliases:
50
- latest: 7.2.1
51
+ latest: 7.2.2
@@ -3,6 +3,16 @@ Release Notes
3
3
 
4
4
  ___
5
5
 
6
+ v7.2.2 (2026-07-01)
7
+ -------------------
8
+
9
+ include_router route resolution now descends the FastAPI wrapper (v7.2.2)
10
+ ------------------------------------------------------------------------
11
+
12
+ - **Fixed** — Per-route decorator config now resolves on `include_router` routes under FastAPI >= 0.115. 7.2.1 resolved routes with `route.matches()` but stopped at FastAPI's `_IncludedRouter` wrapper: its `matches()` returns `Match.FULL` with an empty child-scope and the real routes nested inside. Older FastAPI flattened included routers onto the app (so `matches()` found the leaves directly and CI passed), but newer FastAPI keeps them inside the wrapper — so 7.2.1 resolved nothing there and every per-route decorator (`rate_limit`, `max_request_size`, `content_type_filter`, `custom_validation`, `detection_exclusion`, `suspicious_detection`, `usage_monitor`, `honeypot_detection`, behavioral rules) was still inert. Resolution now descends recursively into matched sub-routers/mounts (`.routes`, `original_router.routes`, `.app.routes`), so per-route config fires on `include_router` and path-parameter routes. Direct routes and single-level `include_router` (prefixed or not) are covered; deeply-nested prefixed routers remain a known limitation of the wrapper's child-scope handling. Supersedes the incomplete 7.2.1 fix.
13
+
14
+ ___
15
+
6
16
  v7.2.1 (2026-07-01)
7
17
  -------------------
8
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_guard
3
- Version: 7.2.1
3
+ Version: 7.2.2
4
4
  Summary: Production-ready security middleware for FastAPI — IP filtering, rate limiting, penetration detection, and 20+ per-route security decorators.
5
5
  Project-URL: Homepage, https://guard-core.com
6
6
  Project-URL: Documentation, https://rennf93.github.io/fastapi-guard/latest/
@@ -373,45 +373,54 @@ class SecurityMiddleware(BaseHTTPMiddleware):
373
373
  route = request.scope.get("route")
374
374
  if route:
375
375
  return route
376
-
377
376
  app = request.scope.get("app")
378
- if not app or not hasattr(app, "routes"):
377
+ routes = getattr(app, "routes", None)
378
+ if not routes:
379
379
  return None
380
+ return self._match_route(routes, request.scope, 0)
380
381
 
382
+ def _match_route(self, routes: Any, scope: Any, depth: int) -> Any:
381
383
  # This middleware is a BaseHTTPMiddleware, so it runs before the router
382
- # and scope["route"] is unset here. Replicate Starlette's own matching
383
- # against the app's routes so per-route decorator config resolves for
384
- # routes with path parameters (e.g. "/items/{id}") and routes added via
385
- # include_router. A plain ``r.path == request.url.path`` comparison never
386
- # matches a templated path, which silently drops per-route config on
387
- # every parameterised route.
388
- for r in app.routes:
389
- if not hasattr(r, "endpoint"):
390
- continue
391
- matches = getattr(r, "matches", None)
392
- if matches is None:
384
+ # and scope["route"] is unset. Replicate Starlette's matching, descending
385
+ # into sub-routers / mounts (FastAPI's include_router inserts a wrapper
386
+ # route whose real routes are nested inside), so per-route decorator
387
+ # config resolves for path-parameter and include_router routes, not only
388
+ # routes flattened directly onto the app.
389
+ for r in routes:
390
+ matched, child = self._route_full_match(r, scope)
391
+ if not matched:
393
392
  continue
394
- try:
395
- match, _ = matches(request.scope)
396
- except Exception:
397
- continue
398
- if match == Match.FULL:
399
- return r
400
-
401
- # Fallback for route objects that do not implement matches().
402
- path = request.url.path
403
- method = request.method
404
- for r in app.routes:
405
- if (
406
- hasattr(r, "path")
407
- and hasattr(r, "methods")
408
- and r.path == path
409
- and method in r.methods
410
- and hasattr(r, "endpoint")
411
- ):
393
+ if hasattr(r, "endpoint"):
412
394
  return r
395
+ if depth >= 8:
396
+ continue
397
+ found = self._match_route(
398
+ self._sub_routes(r), {**scope, **child}, depth + 1
399
+ )
400
+ if found is not None:
401
+ return found
413
402
  return None
414
403
 
404
+ def _route_full_match(self, route: Any, scope: Any) -> tuple[bool, Any]:
405
+ matches = getattr(route, "matches", None)
406
+ if matches is None:
407
+ return False, {}
408
+ try:
409
+ match, child = matches(scope)
410
+ except Exception:
411
+ return False, {}
412
+ return match == Match.FULL, child
413
+
414
+ def _sub_routes(self, route: Any) -> Any:
415
+ included = getattr(route, "original_router", None)
416
+ mounted = getattr(route, "app", None)
417
+ return (
418
+ getattr(route, "routes", None)
419
+ or getattr(included, "routes", None)
420
+ or getattr(mounted, "routes", None)
421
+ or []
422
+ )
423
+
415
424
  def _populate_guard_state(
416
425
  self, guard_request: StarletteGuardRequest, request: Request
417
426
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi_guard"
3
- version = "7.2.1"
3
+ version = "7.2.2"
4
4
  description = "Production-ready security middleware for FastAPI — IP filtering, rate limiting, penetration detection, and 20+ per-route security decorators."
5
5
  authors = [
6
6
  {name = "Renzo Franceschini", email = "rennf93@users.noreply.github.com"}
File without changes
File without changes
File without changes
File without changes
File without changes