From b7a351623234e34f18cf2cd9e05b4b550a1e32dd Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Mon, 27 Jul 2026 09:28:32 -0700 Subject: [PATCH] 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 --- backend/routes/allowlist.py | 4 ++ .../test_otel_exception_handler.py | 37 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/backend/routes/allowlist.py b/backend/routes/allowlist.py index f3a028f5805..a0efa19f320 100644 --- a/backend/routes/allowlist.py +++ b/backend/routes/allowlist.py @@ -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/", diff --git a/tests/test_litellm/integrations/open_telemetry/test_otel_exception_handler.py b/tests/test_litellm/integrations/open_telemetry/test_otel_exception_handler.py index 348ef5082e7..dc99df24c50 100644 --- a/tests/test_litellm/integrations/open_telemetry/test_otel_exception_handler.py +++ b/tests/test_litellm/integrations/open_telemetry/test_otel_exception_handler.py @@ -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",