fix(management): cover the new control plane route in CI's two guards

Both failures are from this branch, not pre-existing

The component allowlist test asserts the gateway and backend route sets union to
the whole app, so any route on neither is a 404 on both pods. Allowlist the
`/management/v1/` prefix on the backend, next to the other control plane
entries, so every resource that moves under it later is covered without a
per-resource edit

The otel handler test builds its request as a SimpleNamespace carrying only
`state`. The validation handler now reads `request.url.path` to decide whether
the caller is on a surface with its own error contract, so the fake needs a url;
a real Request always has one, which is why the handler does not guard for it

The control plane branch returns early, and nothing covered that it still closes
the dangling SERVER span first, so those requests would have leaked a span
apiece. Added a case that pins it; removing the close call fails it
This commit is contained in:
Yuneng Jiang 2026-07-27 09:28:32 -07:00
parent cf127e16e8
commit b7a3516232
No known key found for this signature in database
2 changed files with 33 additions and 8 deletions

View file

@ -70,6 +70,10 @@ BACKEND_PATH_PREFIXES: tuple[str, ...] = (
"/project/",
"/memory/",
"/mcp/",
# Control plane (see the List Endpoints + Tables standard). Every resource
# eventually moves under this prefix, so allowlist it once rather than
# per-resource.
"/management/v1/",
# Spend / analytics
"/spend/",
"/analytics/",

View file

@ -23,11 +23,13 @@ from litellm.integrations._types.open_inference import ErrorAttributes
from ._helpers import assert_server_span_attrs, get_server_span
def _fake_request(parent_otel_span=None):
def _fake_request(parent_otel_span=None, path="/key/generate"):
"""A real Request always carries a url; the validation handler reads its path to
decide whether the caller is on a surface with its own error contract."""
state = types.SimpleNamespace()
if parent_otel_span is not None:
state.parent_otel_span = parent_otel_span
return types.SimpleNamespace(state=state)
return types.SimpleNamespace(state=state, url=types.SimpleNamespace(path=path))
@pytest.fixture
@ -41,7 +43,7 @@ def wired_otel(otel_with_exporter, monkeypatch):
def test_close_dangling_span_stamps_status(
wired_otel, server_span_factory, status, path
):
request = _fake_request(parent_otel_span=server_span_factory(path))
request = _fake_request(parent_otel_span=server_span_factory(path), path=path)
_close_dangling_otel_server_span(request, status)
assert_server_span_attrs(
wired_otel,
@ -59,7 +61,7 @@ def test_close_dangling_span_noop_when_no_span(wired_otel):
def test_close_dangling_span_noop_when_otel_absent(server_span_factory, monkeypatch):
monkeypatch.setattr(proxy_server_module, "open_telemetry_logger", None)
request = _fake_request(parent_otel_span=server_span_factory("/key/generate"))
request = _fake_request(parent_otel_span=server_span_factory("/key/generate"), path="/key/generate")
_close_dangling_otel_server_span(request, 500)
@ -83,7 +85,7 @@ def test_close_dangling_span_noop_when_otel_absent(server_span_factory, monkeypa
def test_exception_handler_closes_span(
wired_otel, server_span_factory, handler, exc, status, path
):
request = _fake_request(parent_otel_span=server_span_factory(path))
request = _fake_request(parent_otel_span=server_span_factory(path), path=path)
response = asyncio.run(handler(request, exc))
assert response.status_code == status
assert_server_span_attrs(
@ -94,6 +96,25 @@ def test_exception_handler_closes_span(
)
def test_validation_handler_closes_span_on_the_control_plane_too(wired_otel, server_span_factory):
"""The control plane answers validation errors with a 400 problem document
instead of the proxy-wide 422, and that branch returns early. It must still
close the dangling SERVER span, or those requests leak a span apiece."""
path = "/management/v1/spend_logs/end_users"
request = _fake_request(parent_otel_span=server_span_factory(path), path=path)
response = asyncio.run(otel_request_validation_exception_handler(request, RequestValidationError(errors=[])))
assert response.status_code == 400
assert response.media_type == "application/problem+json"
assert_server_span_attrs(
wired_otel,
expected_status=400,
expected_url_path=path,
where="otel_request_validation_exception_handler (control plane)",
)
@pytest.mark.parametrize("path", ["/team/list", "/organization/list"])
def test_openai_exception_handler_stamps_structured_error_on_span(
wired_otel, server_span_factory, path
@ -103,7 +124,7 @@ def test_openai_exception_handler_stamps_structured_error_on_span(
ProxyException stringified to "" so error.message was dropped the span
showed an error with no message."""
msg = "Authentication Error, Invalid proxy server token passed."
request = _fake_request(parent_otel_span=server_span_factory(path))
request = _fake_request(parent_otel_span=server_span_factory(path), path=path)
exc = ProxyException(message=msg, type="auth_error", param="key", code=401)
response = asyncio.run(openai_exception_handler(request, exc))
@ -123,7 +144,7 @@ def test_openai_exception_handler_stamps_structured_error_on_span(
def test_unhandled_handler_reraises_known_exceptions(wired_otel, server_span_factory):
"""ProxyException / HTTPException / RequestValidationError have dedicated handlers."""
request = _fake_request(parent_otel_span=server_span_factory("/key/generate"))
request = _fake_request(parent_otel_span=server_span_factory("/key/generate"), path="/key/generate")
with pytest.raises(HTTPException):
asyncio.run(
otel_unhandled_exception_handler(
@ -147,7 +168,7 @@ def test_unhandled_handler_reraises_known_exceptions(wired_otel, server_span_fac
def test_openai_exception_handler_closes_span(
wired_otel, server_span_factory, code, path
):
request = _fake_request(parent_otel_span=server_span_factory(path))
request = _fake_request(parent_otel_span=server_span_factory(path), path=path)
exc = ProxyException(
message="boom",
type="invalid_request_error",