a2aproject / a2aproject/a2a-tck
[Bug]: REST transport streaming fails with "Event loop is closed" error
- Lenguaje dominante
- Python
- Estrellas
- 50
- Forks
- 40
- Merge medio
- 7 d 1 h
- PR fusionados (30 d)
- 1
Descripción
### 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](https://github.com/a2aproject/a2a-tck/blob/main/tck/transport/rest_client.py#L290) for streaming. While the JSON-RPC client creates a [new client](https://github.com/a2aproject/a2a-tck/blob/main/tck/transport/jsonrpc_client.py#L189-L195) 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](https://github.com/a2aproject/a2a-tck/blob/main/pyproject.toml#L67): `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:
```python
async with self.async_client.stream("POST", url, json=payload, headers=headers) as response:
```
After:
```python
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](https://github.com/a2aproject/a2a-js/pull/142) in **a2a-js** sdk. See discussion [here](https://github.com/a2aproject/a2a-js/pull/142#discussion_r2565047768) 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
```shell
====================================================================================================== 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
- [x] I agree to follow this project's Code of Conduct
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.