Skip to content

Commit ec28335

Browse files
declan-scaleclaude
andcommitted
fix(deps): cap fastapi <0.137.0 to stop OPTIONS preflight 500s
FastAPI 0.137.0 made app.routes a tree containing _IncludedRouter wrapper objects that do not expose a .path attribute, instead of a flat list of routes that each have .path. The OTel FastAPI auto-instrumentation (injected at runtime by the observability operator, so not a dependency we can bump here) reads route.path on a Match.PARTIAL while building the request span. A browser CORS preflight (OPTIONS) has no declared endpoint, so it only PARTIAL-matches the _IncludedRouter, raising AttributeError in the ASGI layer (outside FastAPI's exception handlers) and returning HTTP 500. This blocks the Agents UI from loading. The version actually resolved by the workspace is controlled by the root pyproject's override-dependencies, which floored fastapi>=0.135.0 with no ceiling and so picked up 0.137+. Cap it (and the agentex package dep) at <0.137.0. Pinning up does not help: 0.137.1/0.137.2/0.138.0 keep _IncludedRouter and only add a new iter_route_contexts() helper the pinned injected instrumentation does not use. The starlette>=1.3.1 override keeps the existing CVE fixes regardless of the cap; resolution lands on 0.136.3. Add a regression test asserting every app.routes entry exposes .path and that OPTIONS-preflight route matching cannot crash, so a future fastapi bump that reintroduces the regression fails CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8c060f1 commit ec28335

4 files changed

Lines changed: 66 additions & 7 deletions

File tree

agentex/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors = [{ name = "Felix Su", email = "felix.su@scale.com" }]
66
requires-python = ">=3.12,<3.13"
77
readme = "README.md"
88
dependencies = [
9-
"fastapi>=0.115.0",
9+
"fastapi>=0.115.0,<0.137.0", # <0.137.0 keeps app.routes flat; 0.137.0's _IncludedRouter (no .path) crashes injected OTel instrumentation on OPTIONS preflights -> 500
1010
"litellm>=1.84.0,<2", # >=1.84.0 clears CVE-2026-49468 (CRITICAL auth bypass)
1111
"python-dotenv>=1.2.2,<2",
1212
"temporalio>=1.18.0,<2",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Regression test for the OPTIONS/CORS preflight 500 caused by FastAPI's
3+
``_IncludedRouter`` change.
4+
5+
FastAPI 0.137.0 made ``app.routes`` a tree that can contain ``_IncludedRouter``
6+
wrapper objects (one per ``include_router()`` call). Those wrappers do NOT
7+
expose a ``.path`` attribute. The OpenTelemetry FastAPI auto-instrumentation
8+
(injected at runtime by the observability operator, so it is not a dependency we
9+
can bump here) reads ``route.path`` on a ``Match.PARTIAL`` while building the
10+
request span. A browser CORS preflight (``OPTIONS``) has no declared endpoint,
11+
so it only PARTIAL-matches the ``_IncludedRouter`` -> ``AttributeError`` raised
12+
inside the ASGI layer (outside FastAPI's exception handlers) -> HTTP 500.
13+
14+
The only lever we control is the FastAPI version we ship, so we pin
15+
``fastapi < 0.137.0`` to keep ``app.routes`` a flat list of routes that all
16+
expose ``.path``. Pinning *up* does not help: 0.137.1/0.137.2/0.138.0 keep the
17+
``_IncludedRouter`` and only add a new ``iter_route_contexts()`` helper that the
18+
pinned injected instrumentation does not use.
19+
20+
If this test fails, FastAPI has been bumped to a version that reintroduces the
21+
preflight-crash regression for the injected OTel instrumentation.
22+
"""
23+
24+
import pytest
25+
from src.api.app import fastapi_app
26+
from starlette.routing import Match
27+
28+
29+
@pytest.mark.unit
30+
class TestOtelRouteCompat:
31+
def test_every_route_exposes_path(self):
32+
"""Every entry in app.routes must expose ``.path`` (what OTel reads)."""
33+
missing = [
34+
type(route).__name__
35+
for route in fastapi_app.routes
36+
if not hasattr(route, "path")
37+
]
38+
assert not missing, (
39+
"Routes without a `.path` attribute would crash the injected OTel "
40+
f"FastAPI instrumentation on OPTIONS preflights: {missing}. "
41+
"This usually means FastAPI was bumped to >= 0.137.0."
42+
)
43+
44+
def test_options_preflight_route_matching_does_not_crash(self):
45+
"""Simulate OTel `_get_route_details` over an OPTIONS preflight scope."""
46+
scope = {
47+
"type": "http",
48+
"method": "OPTIONS",
49+
"path": "/agents",
50+
"headers": [],
51+
}
52+
for route in fastapi_app.routes:
53+
match, _ = route.matches(scope)
54+
if match in (Match.FULL, Match.PARTIAL):
55+
assert route.path is not None

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ environments = [
2727
# carries the fixes for CVE-2026-48818 / CVE-2026-54283 (and CVE-2025-62727).
2828
# python-multipart>=0.0.32 clears CVE-2026-53539. Keep these floors at the patched
2929
# minimums so a re-resolve can't regress below them.
30+
# fastapi is capped <0.137.0: 0.137.0 made app.routes a tree of _IncludedRouter
31+
# wrappers (no .path), which crashes the injected OTel FastAPI instrumentation on
32+
# OPTIONS/CORS preflights (-> 500). 0.137.1/0.137.2/0.138.0 do not restore the flat
33+
# list. The starlette>=1.3.1 floor below keeps the CVE fixes regardless of this cap.
3034
override-dependencies = [
31-
"fastapi>=0.135.0",
35+
"fastapi>=0.135.0,<0.137.0",
3236
"starlette>=1.3.1",
3337
"httpx[http2]>=0.28.1,<0.29",
3438
"langchain-core>=1.3.3",

uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)