fix(fastapi): fix resource name for starlette and fastapi#18653
Conversation
… name truncation in FastAPI >= 0.137 FastAPI 0.137 changed include_router to use a lazy _IncludedRouter wrapper, causing scope["route"].path_format to return only the leaf-relative path. This breaks resource names for nested routers (e.g. "GET " instead of "GET /v1/items"). Bump the latest lockfile to 0.137.1 to surface the failure in CI and add a targeted test for the missing-prefix case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codeowners resolved as |
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BenchmarksBenchmark execution time: 2026-06-23 16:02:06 Comparing candidate commit 725c5fe in PR branch Found 0 performance improvements and 3 performance regressions! Performance is the same for 617 metrics, 10 unstable metrics. scenario:iastaspects-strip_aspect
scenario:iastaspectsospath-ospathbasename_aspect
scenario:span-start
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove dead else branch — effective_route_context is only set for HTTP routes which always have a method in scope. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9595a1f594
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ctor helpers Merge _build_resource_path into _get_fastapi_effective_path so the function returns the fully-composed path (with mount prefix) or None in one step. When FastAPI is mounted inside a Starlette app, Starlette's Mount copies the scope before delegating so accumulated resource_paths prefixes are lost. Fall back to scope["root_path"] which Starlette sets on the copied scope. Adds test_mounted_fastapi_resource_names to cover this case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The mounted FastAPI resource name fix relies on effective_route_context which only exists in FastAPI >= 0.137. Skip on older versions where the behavior was already broken before this fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The mounted FastAPI-inside-Starlette scenario was already broken before this PR. Remove the test and the root_path fallback since they address a pre-existing issue outside the scope of this fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gle responsibility Remove mount_prefix logic and resource_paths parameter — the function just returns the effective path from FastAPI's scope or None. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mabdinur
left a comment
There was a problem hiding this comment.
Overall the fix is correct and well-structured. A few minor things inline.
- Consolidate and simplify release note - Add AIDEV-NOTE to _is_duplicate_route_call documenting id() assumption - Guard resource_paths accumulation to skip dead state in FastAPI >= 0.137 path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dubloom
left a comment
There was a problem hiding this comment.
I may over focusing on comments but I think it is an important part of keeping the code readable (having there at the right places with only the right information to allow a dev to go efficiently around the code)
Description
FastAPI 0.137 (PR #15745) refactored
APIRouter.include_routerto use a lazy_IncludedRouterwrapper instead of flattening routes onto the parent router at registration time. This introduced two regressions in the ddtrace starlette/fastapi integration:Missing path prefix —
scope["route"].path_formatnow returns only the leaf-relative segment (e.g.""or/{item_id}) instead of the fully-composed template (e.g./v1/items). Spans produced by nestedinclude_routercalls lose their prefix entirely, collapsing allGETrequests into a singleGETbucket in APM.Path doubling —
APIRoute.handlein 0.137 falls back tosuper().handle()(starletteRoute.handle) when noeffective_route_contextis present. When both the fastapi and starlette patches are active,traced_handlerfires twice for the same route instance, doubling the accumulated path (e.g.GET /file/file).Fix in
ddtrace/contrib/internal/starlette/patch.py(traced_handler):effective_route_contextoverride: FastAPI 0.137 setsscope["fastapi"]["effective_route_context"].path_formatto the fully-composed route template before callingAPIRoute.handle. When this key is present, use it directly as the resource name instead of relying on path accumulation frominstance.path.Seen-routes guard: Track route instances processed per request via
scope["datadog"]["_dd_seen_routes"]. Iftraced_handleris entered for an instance already seen in this request, skip accumulation and pass through — preventing the double-counting introduced by thesuper().handle()fallback.The lockfile (
162f3ce) is bumped tofastapi==0.137.1/starlette==1.3.1so thelatestvenv in CI exercises the affected versions.Testing
test_nested_include_router_resource_names— a self-contained test using a two-level nestedinclude_routerthat asserts the full composed path appears in bothresourceandhttp.route(directly reproduces the customer issue in [BUG]: FastAPI/Starlette integration: span resource names degrade to bare GET/POST under FastAPI >= 0.137 (when using lazy include_router) #18621).test_w_patch_starlette(path doubling regression) andtest_subapp_w_starlette_patch_snapshot(snapshot) also pass on 0.137.1.Risks
_dd_seen_routesguard usesid(instance)to deduplicate route calls within a request.id()values can be reused after GC, but route objects are singletons created at app startup and live for the full application lifetime, so collision is not possible in practice.effective_route_contextkey is FastAPI-specific (scope["fastapi"]) and is guarded with.get()so it is a no-op on pure Starlette and older FastAPI versions.Design Decision: Seen-Routes Guard
The conceptually cleaner fix for path doubling would be to skip patching
starlette.routing.Route.handleentirely when the fastapi patch is active, letting the fastapi patch ownAPIRoute.handleexclusively. This was rejected because starlette apps mounted inside FastAPI go throughRoute.handlewithout going throughAPIRoute.handle— removing the starlette patch onRoute.handlewould break resource names for those sub-apps. Tracking seen route instances per request is the safer, more targeted approach.Additional Notes
_dd_seen_routesguard is necessary independently of theeffective_route_contextfix: the former addresses double-firing on plain routes when both patches are active; the latter addresses missing prefixes on nestedinclude_routerroutes. Both are required — verified by running each change in isolation.scope["fastapi"]or callsuper().handle()inAPIRoute.handle, so neither change affects their behaviour.