litellm/tests/test_litellm/proxy/utils
Roman D 55fe4a7894
feat(proxy): resolve root_path per request from a configured prefix list (SERVER_ROOT_PATHS) (#35935)
* feat(proxy): resolve root_path per request from SERVER_ROOT_PATHS

One deployment can encode exactly one client-visible URL path prefix
today: SERVER_ROOT_PATH is a scalar stamped onto the app at startup, so
a pod fronting several ingress prefixes 404s every prefix but one before
any handler runs, and MCP OAuth discovery can emit only one prefix's
URLs (RFC 9728 section 3 exact-match fails for the rest).

Add an opt-in outermost ASGI middleware that matches the request path
against a configured prefix list (SERVER_ROOT_PATHS, comma-separated) on
a segment boundary and sets scope["root_path"] for that request only.
Everything downstream is stock Starlette: route matching strips
root_path so routes stay registered root-relative, and request.base_url
re-includes it, so the discovery documents' resource and the 401
challenges' resource_metadata land under the prefix the client actually
called — with no discovery-builder changes.

LazyFeatureMiddleware now strips the scope root_path (falling back to
the cached SERVER_ROOT_PATH scalar) before feature prefix matching, so
lazily-registered routers — the MCP OAuth discovery router among them —
load under per-request prefixes.

Follow-up to the routing discussion on #35226; composes with, but does
not depend on, #35576.

* fix(proxy): import Sequence from collections.abc (ruff UP035 strict-budget gate)

* review(greptile): trim implementation commentary; fixture-own MCP registry state in tests

Addresses both P2s from the first Greptile pass:
- per_request_root_path_middleware.py (and the related _lazy_features /
  proxy_server comments) cut down to the constraints the code cannot
  express, per repo comment guidance
- the new discovery tests no longer clear/repopulate the shared MCP
  registry inline; a fixture snapshots it, hands the test an empty
  registry, and restores it afterwards so no state leaks between cases

* fix(lint): mutable-ok marker on the prefix accumulator (LIT002 type-discipline gate)

* fix(proxy): tie 401 challenges and get_custom_url to the per-request root_path

The per-request root_path middleware sets scope["root_path"] to the
prefix the client actually called, but the OAuth 401 challenges
(raise_user_oauth_challenge / raise_token_exchange_challenge) still
built their resource_metadata from SERVER_ROOT_PATH. On a pod fronting
several prefixes, the challenge advertised a discovery URL under a
different prefix than the discovery document served — the two
disagreed on where the resource metadata lives, and a strict RFC 9728
client refused the challenge. Route the challenges through a small
ContextVar the middleware populates so they read the same effective
root_path Starlette resolves the request under.

The same accessor fixes get_custom_url: when a request lives under a
SERVER_ROOT_PATHS-matched prefix, request.base_url already carries it,
so appending the SERVER_ROOT_PATH scalar on top produced e.g.
/tenant-a/legacy/sso/callback — a path that does not exist. Reading
the per-request prefix instead (and relying on join_paths's tail-dedup)
keeps SSO login/callback URLs under one prefix — the one the request
actually arrived on.

Fallback: outside a request (module-load-time UI URL builders,
background tasks) the ContextVar is unset and the accessor reads
SERVER_ROOT_PATH, matching get_server_root_path() so scalar-only
deployments are byte-identical.

* fix(mcp): challenge URL under per-request prefix must route, and mock parity

Two follow-ups to the review fix that made the 401 challenge use the
per-request root_path:

1. oauth_protected_resource_path must pick the URL structure that
   actually routes for the mechanism in use:
   - The scalar SERVER_ROOT_PATH deployment registers the well-known
     routes with the prefix INSERTED (via well_known_root_suffix at
     import time), matching RFC 8414 §3. The challenge URL must use the
     same insertion or a client fetching it 404s.
   - The per-request SERVER_ROOT_PATHS deployment can't register routes
     per prefix; PerRequestRootPathMiddleware strips the prefix from
     scope["path"] and the router matches the un-inserted route. The
     URL must place the prefix BEFORE .well-known so the strip leaves a
     matching path.
   The previous fix used the insertion form for both, which 404'd the
   discovery fetch on the per-request path — the discovery doc and the
   challenge would then disagree on where the resource metadata lives,
   the very failure the review flagged. End-to-end verified: the URL
   the challenge advertises routes and the doc's `resource` field
   equals the URL the client originally called (RFC 9728 §3).

2. get_request_root_path now delegates its fallback through
   get_server_root_path() instead of reading the env directly, so every
   existing `monkeypatch.setattr("litellm.proxy.utils.get_server_root_path"`
   test override keeps working. This unstubbed the mock on the /v2/login
   test that failed on the last CI run.

Plus the lint budget: annotate the local accumulator Final, tag the
scope["root_path"] rewrite as an intentional ASGI-contract mutation,
tag the reused `path`/`root_path` rebinds in LazyFeatureMiddleware, and
add reason strings to the two new PLC0415 lazy-import noqas.

* test(mcp): pin the reviewer's expected end-state — challenge URL routes, resource matches called URL

End-to-end regression test that mounts the discoverable router + the
per-request root_path middleware, hits an MCP endpoint that raises
raise_user_oauth_challenge, fetches the resource_metadata URL the
challenge advertises, and checks the returned document's `resource`
equals the URL the client originally called (RFC 9728 §3 exact match).

Covers /tenant-a, /tenant-b, and the unprefixed path on the same app so
a regression on any prefix — challenge URL 404s, or doc emits a
different prefix than the client called — fails at this test rather
than in a strict MCP client's discovery.

---------

Co-authored-by: gym-cmd <186399764+gym-cmd@users.noreply.github.com>
2026-09-07 11:54:59 -07:00
..
helpers feat(proxy): resolve root_path per request from a configured prefix list (SERVER_ROOT_PATHS) (#35935) 2026-09-07 11:54:59 -07:00
prisma_and_spend fix(proxy): retry deadlocks and requeue spend logs on any DB write error (#39883) 2026-09-05 11:41:56 -07:00
proxy_logging Merge pull request #38440 from BerriAI/litellm_prompt_registry_env 2026-09-03 14:09:26 -07:00
__init__.py test(proxy/utils): pin bottom-of-file helper behavior (#29509) 2026-06-02 17:45:19 -07:00