Run StreamableHTTP transport tests in process instead of over sockets (#2767)

This commit is contained in:
Max 2026-06-03 12:45:00 +01:00 • committed by GitHub
commit 19fe9faec8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 969 additions and 1177 deletions

View file

@ -207,7 +207,7 @@ class StreamableHTTPServerTransport:
send_stream.close() send_stream.close()
receive_stream.close() receive_stream.close()
def close_standalone_sse_stream(self) -> None: # pragma: no cover def close_standalone_sse_stream(self) -> None:
"""Close the standalone GET SSE stream, triggering client reconnection. """Close the standalone GET SSE stream, triggering client reconnection.
This method closes the HTTP connection for the standalone GET stream used This method closes the HTTP connection for the standalone GET stream used
@ -221,8 +221,6 @@ class StreamableHTTPServerTransport:
This is a no-op if there is no active standalone SSE stream. This is a no-op if there is no active standalone SSE stream.
Requires event_store to be configured for events to be stored during Requires event_store to be configured for events to be stored during
the disconnect. the disconnect.
Currently, client reconnection for standalone GET streams is NOT
implemented - this is a known gap (see test_standalone_get_stream_reconnection).
""" """
self.close_sse_stream(GET_STREAM_KEY) self.close_sse_stream(GET_STREAM_KEY)
@ -245,7 +243,7 @@ class StreamableHTTPServerTransport:
async def close_stream_callback() -> None: async def close_stream_callback() -> None:
self.close_sse_stream(request_id) self.close_sse_stream(request_id)
async def close_standalone_stream_callback() -> None: # pragma: no cover async def close_standalone_stream_callback() -> None:
self.close_standalone_sse_stream() self.close_standalone_sse_stream()
metadata = ServerMessageMetadata( metadata = ServerMessageMetadata(
@ -421,7 +419,7 @@ class StreamableHTTPServerTransport:
has_json, has_sse = self._check_accept_headers(request) has_json, has_sse = self._check_accept_headers(request)
if self.is_json_response_enabled: if self.is_json_response_enabled:
# For JSON-only responses, only require application/json # For JSON-only responses, only require application/json
if not has_json: # pragma: no cover if not has_json:
response = self._create_error_response( response = self._create_error_response(
"Not Acceptable: Client must accept application/json", "Not Acceptable: Client must accept application/json",
HTTPStatus.NOT_ACCEPTABLE, HTTPStatus.NOT_ACCEPTABLE,

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@
import socket import socket
import threading import threading
import time
from collections.abc import Generator from collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any from typing import Any
@ -56,30 +55,3 @@ def run_uvicorn_in_thread(app: Any, **config_kwargs: Any) -> Generator[str, None
finally: finally:
server.should_exit = True server.should_exit = True
thread.join(timeout=_SERVER_SHUTDOWN_TIMEOUT_S) thread.join(timeout=_SERVER_SHUTDOWN_TIMEOUT_S)
def wait_for_server(port: int, timeout: float = 20.0) -> None:
"""Wait for server to be ready to accept connections.
Polls the server port until it accepts connections or timeout is reached.
This eliminates race conditions without arbitrary sleeps.
Args:
port: The port number to check
timeout: Maximum time to wait in seconds (default 5.0)
Raises:
TimeoutError: If server doesn't start within the timeout period
"""
start_time = time.time()
while time.time() - start_time < timeout:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.1)
s.connect(("127.0.0.1", port))
# Server is ready
return
except (ConnectionRefusedError, OSError):
# Server not ready yet, retry quickly
time.sleep(0.01)
raise TimeoutError(f"Server on port {port} did not start within {timeout} seconds") # pragma: no cover