Release context.lock before the protected request is sent

Closed
GU
2mo ago
+128-7

Fixes #3209

async_auth_flow held context.lock across response = yield request, so the lock covered the
whole round trip of the protected request rather than just token acquisition. The standalone GET
SSE stream runs through the same provider, so it pinned the lock for the lifetime of the stream
and the next request, typically the first tools/call, blocked in lock.acquire() until the
stream ended.

The lock now closes before the request is yielded and re-opens around the 401 and 403
re-authorization blocks. Token acquisition and refresh are still serialized; no protected request
is sent while the lock is held.

New test: tests/client/test_auth.py::test_in_flight_request_does_not_block_a_concurrent_request.
It drives two auth flows from two anyio tasks, holds the GET flow at its yield, and requires the
POST flow to reach its own yield inside anyio.fail_after(5). Without the patch it fails with
TimeoutError parked on async with self.context.lock; with it, it passes in 0.26s.

Verified on Linux, CPU only, Python 3.13.14:

  • pytest tests/client/test_auth.py tests/client/auth tests/client/test_streamable_http.py -> 197 passed, 1 xfailed
  • ./scripts/test -> 100.00% branch coverage, strict-no-cover clean
  • ruff format --check ., ruff check ., pyright all clean

Not verified: the reporter's ~15s figure against a live server. I reproduced the mechanism, not
the deployment. The multi-second stall needs a server or proxy that does not flush the GET SSE
response headers promptly, which is the Cloud Run behaviour in the issue; against a transport that
returns headers immediately, httpx2 resumes the generator right away and the stall does not show
up. Only Python 3.13 on Linux, asyncio backend, was exercised locally.

One knowing behaviour change: two requests in flight at once can now both get a 401 and both run a
full authorization. Before, the second could not be sent until the first finished, which hid this.
#2858 handles that with a "did the token change while I was in flight?" check. I left it out to
keep this change to the reported bug, so it is worth a follow-up.

Thanks to @stayclosetothequestion for the report, the measurements, and for tracing it to the
exact line.