Skip to content

fix(mcp-oauth): anchor 401 handler task to prevent GC mid-flight - #55952

Merged
teknium1 merged 2 commits into
mainfrom
salvage/mcp-oauth-401-gc-12228
Jun 30, 2026
Merged

fix(mcp-oauth): anchor 401 handler task to prevent GC mid-flight#55952
teknium1 merged 2 commits into
mainfrom
salvage/mcp-oauth-401-gc-12228

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Salvage of #12228 (@haileymarshall) onto current main. Anchors the MCP OAuth 401-recovery task to a strong-reference set so the event loop can't garbage-collect it mid-run.

Root cause

MCPOAuthManager.handle_401() launched its deduped recovery coroutine via asyncio.create_task(_do_handle()) and dropped the handle. The event loop only weakly references tasks, so _do_handle could be GC'd between its await checkpoints. When that happened:

  • pending.set_result(...) never ran → every concurrent 401 caller stuck on await pending hung forever.
  • The finally: entry.pending_401.pop(key, None) cleanup never ran → subsequent 401s for the same access token latched onto the dead future, poisoning the manager for that key until process restart.

Same fire-and-forget pattern already fixed on the adapter side (#11997, #11998, #12000, #12001, #12006); mcp_oauth_manager.py was missed.

Changes

  • tools/mcp_oauth_manager.py: add self._inflight_tasks: set[asyncio.Task] in __init__; in handle_401, self._inflight_tasks.add(task) on launch and task.add_done_callback(self._inflight_tasks.discard) on completion.
  • tests/tools/test_mcp_oauth_manager.py: two new tests (inflight tracking + 8-caller dedup-survival with gc.collect() mid-flight).

Follow-up fix on top of the contributor commit

The two new tests asserted _inflight_tasks was empty immediately after handle_401 returned, but the discard done-callback is scheduled via loop.call_soon and runs on a later loop iteration than the one that resolves pending. Added a single await asyncio.sleep(0) before the cleanup assertions in both tests to let the callback fire.

Validation

Before After
Concurrent 401 waiters when task GC'd hang forever all resolve
dedup key after GC poisoned until restart cleaned up
tests/tools/test_mcp_oauth_manager.py 16 passed

Infographic

infographic

haileymarshall and others added 2 commits June 30, 2026 16:43
`handle_401` spawned a dedup'd recovery coroutine via
`asyncio.create_task(_do_handle())` and discarded the returned task
reference. Python's event loop only keeps weak references to tasks, so
the coroutine could be garbage-collected before it called
`pending.set_result(...)`. Every concurrent caller awaiting that future
then hangs forever, and the `finally: entry.pending_401.pop(...)`
cleanup never runs — so subsequent 401s for the same key latch onto the
dead future too. Same pattern the adapter-side fixes address (#11997,
#11998, #12000, #12001, #12006).

Hold the task in a process-wide set on the manager and discard it via
`add_done_callback` once it completes. Regression test covers both the
structural invariant (task tracked, then removed on completion) and a
concurrent dedup path with a forced `gc.collect()` between the handler's
await points.
The discard done-callback added via task.add_done_callback runs on a later
event-loop iteration (call_soon) than the one that resolves `pending` and lets
handle_401 return. Both inflight-task tests asserted the live set was empty
immediately after the await returned, racing the callback. Add a single
`await asyncio.sleep(0)` before the cleanup assertions.
@alt-glitch alt-glitch added type/bug Something isn't working tool/mcp MCP client and OAuth P3 Low — cosmetic, nice to have duplicate This issue or pull request already exists labels Jun 30, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #51757 — byte-identical salvage of #12228 (same tools/mcp_oauth_manager.py _inflight_tasks + add_done_callback fix and the same regression tests). #51757 is the earlier open salvage and is canonical; this one duplicates it. Related to the original predecessor #12228. Maintainer to pick one of the two competing salvages.

@teknium1
teknium1 merged commit 20ca2d5 into main Jun 30, 2026
31 checks passed
@teknium1
teknium1 deleted the salvage/mcp-oauth-401-gc-12228 branch June 30, 2026 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants