From d988315d02239c08ab49bc9f01900a3e2d83e515 Mon Sep 17 00:00:00 2001 From: Francisco Rivas Date: Fri, 10 Apr 2026 14:43:08 +0200 Subject: [PATCH 1/3] fix: SSE /stream/ui-updates returns 422 due to Request alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With 'from __future__ import annotations' active, FastAPI resolves type annotations as strings at startup. Importing 'Request as _Request' inside the enable_streaming block caused FastAPI to see the annotation '_Request' as an unknown type and treat 'request' as a required query parameter, returning 422 Unprocessable Content on every SSE connection attempt — showing the 'Disconnected' badge in the UI. Fix: import Request at module level without alias so FastAPI can resolve it correctly as fastapi.Request regardless of annotation evaluation mode. --- src/agentevals/api/app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/agentevals/api/app.py b/src/agentevals/api/app.py index 7a8cf59..8d64795 100644 --- a/src/agentevals/api/app.py +++ b/src/agentevals/api/app.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse @@ -91,7 +91,6 @@ def create_app( if trace_manager is None: raise ValueError("enable_streaming requires a trace_manager") - from fastapi import Request as _Request from fastapi import WebSocket from .streaming_routes import streaming_router @@ -103,7 +102,7 @@ async def websocket_endpoint(websocket: WebSocket): await websocket.app.state.trace_manager.handle_connection(websocket) @app.get("/stream/ui-updates") - async def ui_updates_stream(request: _Request): + async def ui_updates_stream(request: Request): mgr = request.app.state.trace_manager async def event_generator(): From 87bd6c8d60f580d499faec4445d3cbf50f724a4f Mon Sep 17 00:00:00 2001 From: Francisco Rivas Date: Fri, 10 Apr 2026 19:31:53 +0200 Subject: [PATCH 2/3] test: add regression test for SSE /stream/ui-updates 422 bug Verifies that GET /stream/ui-updates returns 200 and text/event-stream content type. Without the fix (importing Request at module level), FastAPI resolves the '_Request' annotation as a required query parameter and returns 422 on every connection attempt. --- tests/test_api.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 452d181..7f3d901 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1023,3 +1023,51 @@ def test_load_success(self): assert body["data"]["count"] == 1 assert "sess1" in body["data"]["loadedSessions"] _assert_all_keys_camel(body) + + +# --------------------------------------------------------------------------- +# GET /stream/ui-updates (SSE) +# --------------------------------------------------------------------------- + + +class TestUIUpdatesSSE: + """Regression test for the _Request alias bug. + + With ``from __future__ import annotations`` active, importing + ``Request as _Request`` inside the ``enable_streaming`` block caused + FastAPI to treat ``request`` as a required query parameter, returning + 422 on every SSE connection attempt. The fix moves the import to + module level without an alias. + """ + + def _make_streaming_app(self): + import asyncio + + from agentevals.api.app import create_app + from agentevals.streaming.ws_server import StreamingTraceManager + + mgr = StreamingTraceManager() + # Replace register_sse_client so the queue immediately closes (None sentinel) + # so the streaming response can be read synchronously in tests. + q: asyncio.Queue = asyncio.Queue() + q.put_nowait(None) + mgr.register_sse_client = MagicMock(return_value=q) + mgr.unregister_sse_client = MagicMock() + return create_app(enable_streaming=True, trace_manager=mgr) + + def test_sse_endpoint_returns_200_not_422(self): + """GET /stream/ui-updates must return 200, not 422.""" + app = self._make_streaming_app() + client = TestClient(app, raise_server_exceptions=False) + resp = client.get("/stream/ui-updates") + assert resp.status_code == 200, ( + f"Expected 200 but got {resp.status_code}. " + "A 422 indicates the Request type annotation was not resolved correctly." + ) + + def test_sse_endpoint_content_type(self): + """The response must use the text/event-stream media type.""" + app = self._make_streaming_app() + client = TestClient(app, raise_server_exceptions=False) + resp = client.get("/stream/ui-updates") + assert resp.headers["content-type"].startswith("text/event-stream") From 624a045495aefb21646b37c5311cdd0a8d89100d Mon Sep 17 00:00:00 2001 From: Francisco Rivas Date: Mon, 13 Apr 2026 12:28:25 +0200 Subject: [PATCH 3/3] fix: address SSE request and test review suggestions --- src/agentevals/api/app.py | 3 +-- tests/test_api.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/agentevals/api/app.py b/src/agentevals/api/app.py index 8d64795..25f827b 100644 --- a/src/agentevals/api/app.py +++ b/src/agentevals/api/app.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING from fastapi import FastAPI, Request +from fastapi import WebSocket from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse @@ -91,8 +92,6 @@ def create_app( if trace_manager is None: raise ValueError("enable_streaming requires a trace_manager") - from fastapi import WebSocket - from .streaming_routes import streaming_router app.include_router(streaming_router, prefix="/api/streaming") diff --git a/tests/test_api.py b/tests/test_api.py index 7f3d901..bf3abee 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1058,7 +1058,7 @@ def _make_streaming_app(self): def test_sse_endpoint_returns_200_not_422(self): """GET /stream/ui-updates must return 200, not 422.""" app = self._make_streaming_app() - client = TestClient(app, raise_server_exceptions=False) + client = TestClient(app) resp = client.get("/stream/ui-updates") assert resp.status_code == 200, ( f"Expected 200 but got {resp.status_code}. " @@ -1068,6 +1068,6 @@ def test_sse_endpoint_returns_200_not_422(self): def test_sse_endpoint_content_type(self): """The response must use the text/event-stream media type.""" app = self._make_streaming_app() - client = TestClient(app, raise_server_exceptions=False) + client = TestClient(app) resp = client.get("/stream/ui-updates") assert resp.headers["content-type"].startswith("text/event-stream")