Skip to content

[Bug]: REST transport streaming fails with "Event loop is closed" error #99

@tchapacan

Description

@tchapacan

What happened?

The REST client's streaming methods (send_streaming_message and subscribe_to_task) fail intermittently with RuntimeError: Event loop is closed during async cleanup.

Possible Root Cause

The REST client re-uses a shared AsyncClient for streaming. While the JSON-RPC client creates a new client per request. When pytest-asyncio creates a new event loop for each test (default behavior: asyncio_default_test_loop_scope=function), the shared AsyncClient from a previous test can become bound to a closed event loop, causing the cleanup errors for the REST scenario.

Possible Solutions

Option 1: Change pytest-asyncio loop scope. Add to pyproject.toml: asyncio_default_test_loop_scope = "session"
This shares a single event loop across all tests, keeping the shared client valid. However, this seems a workaround that will affects all tests as this option is global and may have unintended side effects. Don't know if it's the cleanest solution.

Option 2: Create new client per request
Update REST streaming methods to match JSON-RPC behavior

Before:

async with self.async_client.stream("POST", url, json=payload, headers=headers) as response:

After:

async with AsyncClient(timeout=self.timeout, verify=self._create_ssl_context()) as client:
                async with client.stream("POST", url, json=payload, headers=headers) as response:

I let you analyze this issue that was discovered while working on this PR in a2a-js sdk. See discussion here for more details. LMK if you need further details to help you debug the situation. If analysis is valid and possible solutions seems acceptable to you, I'd rather go with option 2, but I let you decide, just a proposition.

Relevant log output

====================================================================================================== FAILURES ======================================================================================================
__________________________________________________________________________________________ test_sse_event_format_compliance __________________________________________________________________________________________
tck/transport/rest_client.py:284: in send_streaming_message
    async with self.async_client.stream("POST", url, json=payload, headers=headers) as response:
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/contextlib.py:214: in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpx/_client.py:1583: in stream
    response = await self.send(
.venv/lib/python3.14/site-packages/httpx/_client.py:1629: in send
    response = await self._send_handling_auth(
.venv/lib/python3.14/site-packages/httpx/_client.py:1657: in _send_handling_auth
    response = await self._send_handling_redirects(
.venv/lib/python3.14/site-packages/httpx/_client.py:1694: in _send_handling_redirects
    response = await self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpx/_client.py:1730: in _send_single_request
    response = await transport.handle_async_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpx/_transports/default.py:394: in handle_async_request
    resp = await self._pool.handle_async_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpcore/_async/connection_pool.py:256: in handle_async_request
    raise exc from None
.venv/lib/python3.14/site-packages/httpcore/_async/connection_pool.py:236: in handle_async_request
    response = await connection.handle_async_request(
.venv/lib/python3.14/site-packages/httpcore/_async/connection.py:103: in handle_async_request
    return await self._connection.handle_async_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpcore/_async/http11.py:135: in handle_async_request
    await self._response_closed()
.venv/lib/python3.14/site-packages/httpcore/_async/http11.py:250: in _response_closed
    await self.aclose()
.venv/lib/python3.14/site-packages/httpcore/_async/http11.py:258: in aclose
    await self._network_stream.aclose()
.venv/lib/python3.14/site-packages/httpcore/_backends/anyio.py:53: in aclose
    await self._stream.aclose()
.venv/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1352: in aclose
    self._transport.close()
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/selector_events.py:1209: in close
    super().close()
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/selector_events.py:869: in close
    self._loop.call_soon(self._call_connection_lost, None)
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/base_events.py:827: in call_soon
    self._check_closed()
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/base_events.py:550: in _check_closed
    raise RuntimeError('Event loop is closed')
E   RuntimeError: Event loop is closed

During handling of the above exception, another exception occurred:
tests/optional/capabilities/test_streaming_methods.py:589: in test_sse_event_format_compliance
    async for event in stream:
tck/transport/rest_client.py:329: in send_streaming_message
    raise TransportError(error_msg, TransportType.REST)
E   tck.transport.base_client.TransportError: [REST] Unexpected error in REST streaming: Event loop is closed
------------------------------------------------------------------------------------------------- Captured log call --------------------------------------------------------------------------------------------------
ERROR    tck.transport.rest_client:rest_client.py:328 Unexpected error in REST streaming: Event loop is closed
============================================================================================== short test summary info ===============================================================================================
FAILED tests/optional/capabilities/test_streaming_methods.py::test_sse_event_format_compliance - tck.transport.base_client.TransportError: [REST] Unexpected error in REST streaming: Event loop is closed
================================================================================ 1 failed, 20 passed, 52 skipped, 1 xfailed in 51.61s ================================================================================
⬅️  [rest] Exit code: 1

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions